diff --git a/.github/workflows/gtest.yml b/.github/workflows/gtest.yml index d18c4144..85289b6f 100644 --- a/.github/workflows/gtest.yml +++ b/.github/workflows/gtest.yml @@ -16,8 +16,7 @@ jobs: env: WS_DIR: ${{ github.workspace }}/ws SRC_DIR: ${{ github.workspace }}/ws/src - IMAGE_URL: https://pub-997cd3005edc4b9db91df913907990bf.r2.dev/autoaim/model_infer_example.jpg - IMAGE_PATH: /tmp/model_infer_example.jpg + TEST_ASSETS_ROOT: /tmp/auto_aim steps: - name: Checkout repository into ROS2 workspace @@ -26,9 +25,16 @@ jobs: repository: Alliance-Algorithm/rmcs_auto_aim_v2 path: ws/src/rmcs_auto_aim_v2 - - name: Download test frame + - name: Install yq dependency run: | - curl -fsSL "$IMAGE_URL" -o "$IMAGE_PATH" + apt-get update + apt-get install -y yq + + - name: Download test assets + shell: bash + run: | + cd "$SRC_DIR/rmcs_auto_aim_v2/test" + TEST_ASSETS_ROOT="$TEST_ASSETS_ROOT" ./download_assets.sh - name: Configure test project shell: bash @@ -49,4 +55,4 @@ jobs: run: | source /opt/ros/jazzy/setup.bash cd "$SRC_DIR/rmcs_auto_aim_v2/test" - IMAGE="$IMAGE_PATH" ctest --test-dir build --output-on-failure + ctest --test-dir build --output-on-failure diff --git a/README.md b/README.md index 60d63340..f8108395 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,18 @@ 本项目以工程化为最终目的,为机器人提供一个测试与工作流完备,配置友好,重构开销小,错误提示拟人的自瞄系统,方便队员的后续维护和持续开发,为迭代提供舒适的代码基础 +## 核心概念 + +依赖隐藏: + +非侵入式: + +提前编写期检查: + +推迟运行时多态: + +自动化与测试: + ## 部署步骤 先确保海康相机的 SDK 正确构建,再保证 `rmcs_exetutor` 正确构建,如果要运行 RMCS 控制系统的话 @@ -145,7 +157,7 @@ ros2 launch rmcs_auto_aim_v2 launch.py [...] [INFO] [...] [visualization]: Sdp has been written to: /tmp/auto_aim.sdp ``` -随后在本机下载 `VLC`,此外,还需要下载插件:`vlc-plugin-live555` 和 `vlc-plugin-ffmpeg` 以支持播放推流 +对于 `VLC`,需要下载插件:`vlc-plugin-live555` 和 `vlc-plugin-ffmpeg` 接下来只需要将 `/tmp/auto_aim.sdp` 文件拷贝到自己电脑上,使用能够打开`SDP`文件的视频播放器打开即可,也可以使用指令: diff --git a/config/config.yaml b/config/config.yaml index ec76c174..ea3d446f 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -5,14 +5,14 @@ capturer: show_loss_framerate: false show_loss_framerate_interval: 500 reconnect_wait_interval: 100 - source: "hikcamera" + source: "local_video" hikcamera: # int timeout_ms: 500 # float - exposure_us: 3000.0 + exposure_us: 2000.0 # float - framerate: 120 + framerate: 60 # float gain: 16.9807 @@ -21,8 +21,14 @@ capturer: trigger_mode: false fixed_framerate: true local_video: - location: "" + # 替换为你具体的路径 + location: "/workspaces/alliance/test_videos/solve_pnp_v2.mp4" + # double 帧率 + frame_rate: 60 + # bool 是否循环播放 loop_play: true + # bool 是否允许跳帧以满足实时性 + allow_skipping: false identifier: binarization_threshold: 0.5 @@ -41,8 +47,8 @@ identifier: nms_threshold: 0.3 pose_estimator: - camera_matrix: [2414.9359264386621, 0, 717.26243105567414, 0, 2418.0489262208148, 582.68540529942845, 0, 0, 1] - distort_coeff: [-0.0209453389287673, 0.15028138841073832, -0.0006517722113234505, -0.0016861906197686788, 0] + camera_matrix: [1.722231837421459e+03, 0, 7.013056440882832e+02, 0, 1.724876404292754e+03,5.645821718351237e+02 , 0, 0, 1] + distort_coeff: [-0.064232403853946, -0.087667493884102, 0,0, 0.792381808294582] transforms: - parent: "imu_link" diff --git a/doc/utility.md b/doc/utility.md new file mode 100644 index 00000000..1e0d6f5e --- /dev/null +++ b/doc/utility.md @@ -0,0 +1,98 @@ +# Utility 工具库文档 + +本文档描述了 `utility` 工具库中各个模块的功能和用途。 + +## 设计理念 + +本工具库中的许多组件是为了**隐藏第三方库的头文件**而设计的包装器。第三方库(如 ROS2 的 rclcpp、OpenCV、Eigen、PCL 等)的头文件会增加编译成本,影响编译体验。通过使用包装器提供有限确定的接口,可以: + +- **减少编译时间**:避免在头文件中包含大型第三方库头文件 +- **降低编译依赖**:使用 PIMPL 模式将实现细节隐藏在 `.cpp` 文件中 +- **保持接口稳定**:提供简洁、稳定的接口,减少对第三方库内部变化的依赖 +- **改善编译体验**:只暴露必要的接口,减少不必要的类型暴露 + +因此,建议优先使用这些包装器而不是直接包含原始第三方库头文件。 + +## 核心工具 + +### 线性代数 +- **[linear.hpp](../src/utility/math/linear.hpp)**: 提供 `Translation` 和 `Orientation` 结构体,用于表示三维空间中的平移和旋转。支持从符合特定 trait 的对象进行转换,并提供 `copy_to` 方法将数据复制到目标对象。 + +### 错误处理 +- **[panic.hpp](../src/utility/panic.hpp)** / **[panic.cpp](../src/utility/panic.cpp)**: 提供 `panic` 函数,用于程序异常终止。会输出详细的错误信息,包括消息、文件位置、函数名、行号、线程 ID、时间戳和堆栈跟踪。 + +### 设计模式 +- **[pimpl.hpp](../src/utility/pimpl.hpp)**: 提供 PIMPL(Pointer to Implementation)模式的宏定义 `RMCS_PIMPL_DEFINITION`,用于隐藏实现细节。 +- **[details.hpp](../src/utility/details.hpp)**: 提供 Details 模式的宏定义 `RMCS_DETAILS_DEFINITION`,用于分离接口和实现细节。 + +### 序列化 +- **[serializable.hpp](../src/utility/serializable.hpp)**: 提供序列化框架,支持从 YAML 节点或 rclcpp 节点读取参数。使用 `Serializable` 结构体和 `MemberMeta` 来定义可序列化的成员,支持类型安全的参数读取。 + +### 次数限制 +- **[times_limit.hpp](../src/utility/times_limit.hpp)**: 提供 `TimesLimit` 类,用于限制某个操作的执行次数。支持启用/禁用、重置计数等功能。 + +### 图像处理(OpenCV 包装器) +- **[image.hpp](../src/utility/image/image.hpp)** / **[image.cpp](../src/utility/image/image.cpp)**: 提供 `Image` 类,封装图像数据和时间戳。使用 PIMPL 模式隐藏 OpenCV 头文件,避免在头文件中暴露 `cv::Mat` 等 OpenCV 类型。 +- **[image.details.hpp](../src/utility/image/image.details.hpp)**: 定义 `Image::Details` 结构体,包含 OpenCV 的 `cv::Mat` 和相关的访问方法。此文件包含 OpenCV 头文件,应仅在实现文件中使用。 +- **[image/painter.hpp](../src/utility/image/image/painter.hpp)**: 图像绘制工具(具体实现需查看源码)。 + +### ROS2 节点(rclcpp 包装器) +- **[node.hpp](../src/utility/node.hpp)**: 提供 `Node` 类,继承自 `rclcpp::Node`,扩展了日志功能,支持使用 `std::format` 进行格式化输出。提供 `info`、`warn`、`error` 等方法。**注意**:此文件直接继承 rclcpp::Node,如需完全隐藏 rclcpp 头文件,请使用 `rclcpp/node.hpp` 中的 `RclcppNode`。 + +### 鸭子类型 +- **[duck_type.hpp](../src/utility/duck_type.hpp)**: 提供 `duck_array` 模板类,实现鸭子类型检查。支持在编译时验证类型是否满足特定接口要求,并提供类型安全的元素访问。 + +## 子模块 + +### 协程 (coroutine) +- **[coroutine/channel.hpp](../src/utility/coroutine/channel.hpp)**: 提供协程通道 `Channel`,用于协程之间的数据传递。支持发送和接收操作,当通道为空时自动挂起等待。 +- **[coroutine/common.hpp](../src/utility/coroutine/common.hpp)**: 提供协程任务 `task` 模板,用于管理协程的生命周期。支持立即启动、延迟销毁,并能处理异常。 +- **[coroutine/context.hpp](../src/utility/coroutine/context.hpp)**: 定义协程上下文类(当前为空实现)。 + +### 数学工具 (math) +- **[math/sigmoid.hpp](../src/utility/math/sigmoid.hpp)**: 提供 sigmoid 函数实现,用于数值稳定计算。 +- **[math/solve_armors.hpp](../src/utility/math/solve_armors.hpp)**: 提供装甲板正解和逆解算法。`ArmorsForwardSolution` 用于根据机器人位姿计算四个装甲板的位置和姿态。 +- **[math/solve_armors.cpp](../src/utility/math/solve_armors.cpp)**: 正解算法的实现。 +- **[math/solve_pnp.hpp](../src/utility/math/solve_pnp.hpp)**: 提供将相机内外参的转换函数供pnp解算使用。 + +### ROS2 扩展 (rclcpp 包装器) +- **[rclcpp/node.hpp](../src/utility/rclcpp/node.hpp)**: 提供 `RclcppNode` 类,封装 ROS2 节点的基本功能,包括日志、发布主题前缀管理等。**使用 PIMPL 模式完全隐藏 rclcpp 头文件**,推荐在需要避免编译依赖时使用。 +- **[rclcpp/node.cpp](../src/utility/rclcpp/node.cpp)**: `RclcppNode` 的实现,包含所有 rclcpp 相关的头文件。 +- **[rclcpp/node.details.hpp](../src/utility/rclcpp/node.details.hpp)**: `RclcppNode::Details` 的定义,包含 rclcpp 类型,应仅在实现文件中使用。 +- **[rclcpp/parameters.hpp](../src/utility/rclcpp/parameters.hpp)**: 提供参数接口 `IParams` 和参数管理类 `Parameters`,用于统一参数访问接口。**隐藏 rclcpp 参数访问的细节**。 +- **[rclcpp/parameters.cpp](../src/utility/rclcpp/parameters.cpp)**: `Parameters` 的实现,包含 rclcpp 头文件。 +- **[rclcpp/rclcpp_param.hpp](../src/utility/rclcpp/rclcpp_param.hpp)**: 提供 `make_params` 函数,用于从 rclcpp 节点创建参数接口实现。**此文件包含 rclcpp 头文件**,应在实现文件中使用。 +- **[rclcpp/configuration.hpp](../src/utility/rclcpp/configuration.hpp)**: 提供 `configuration` 函数,用于从 YAML 文件加载配置。**此文件包含 YAML-CPP 头文件**。 +- **[rclcpp/visual/armor.hpp](../src/utility/rclcpp/visual/armor.hpp)** / **[rclcpp/visual/armor.cpp](../src/utility/rclcpp/visual/armor.cpp)**: ROS2 可视化消息相关(装甲板可视化),封装 ROS2 可视化消息类型。 + +### 机器人相关 (robot) +- **[robot/armor.hpp](../src/utility/robot/armor.hpp)**: 定义装甲板相关类型,包括 `ArmorColor`(颜色枚举)、`ArmorShape`(形状枚举)、`ArmorType`(类型)、`LightStrip`(灯条)和 `Armor` 结构体。 +- **[robot/color.hpp](../src/utility/robot/color.hpp)**: 定义 `CampColor` 枚举,表示阵营颜色(未知、红色、蓝色)。 +- **[robot/id.hpp](../src/utility/robot/id.hpp)**: 定义 `DeviceId` 枚举和 `DeviceIds` 类,用于标识不同类型的机器人设备(英雄、工程、步兵、哨兵等)。提供设备 ID 的位操作和查询功能。 + +### 日志 (logging) +- **[logging/printer.hpp](../src/utility/logging/printer.hpp)**: 提供 `Printer` 类,用于日志输出。支持不同级别的日志(INFO、WARN、ERROR),使用 `std::format` 进行格式化。 +- **[logging/printer.cpp](../src/utility/logging/printer.cpp)**: `Printer` 的实现。 + +### 共享内存 (shared) +- **[shared/context.hpp](../src/utility/shared/context.hpp)**: 定义共享上下文结构体,包含时间戳和字节数组,用于进程间数据共享。 +- **[shared/interprocess.hpp](../src/utility/shared/interprocess.hpp)**: 提供进程间通信的共享内存客户端。`Client` 模板类包含 `Send` 和 `Recv` 两个类,分别用于发送和接收数据。使用版本号机制确保数据一致性。 + +### 单例模式 (singleton) +- **[singleton/running.hpp](../src/utility/singleton/running.hpp)**: 提供运行状态管理的全局函数 `get_running` 和 `set_running`。 +- **[singleton/running.cpp](../src/utility/singleton/running.cpp)**: 运行状态管理的实现。 + +### 线程工具 (thread) +- **[thread/spsc_queue.hpp](../src/utility/thread/spsc_queue.hpp)**: 提供单生产者单消费者(SPSC)无锁队列的别名定义,基于 Boost.Lockfree 库。 +- **[thread/workers.hpp](../src/utility/thread/workers.hpp)**: 提供 `WorkersContext` 类,用于管理工作线程池。支持提交任务并返回 future,任务必须是 noexcept 可调用的。 +- **[thread/workers.cpp](../src/utility/thread/workers.cpp)**: `WorkersContext` 的实现。 + +### 模型 (model) +- **[model/armor_detection.hpp](../src/utility/model/armor_detection.hpp)**: 定义 `ArmorDetection` 结构体,用于表示检测到的装甲板信息。包含角点坐标、置信度、颜色信息和角色信息。支持从原始数据直接反序列化,并提供边界框计算和角点缩放功能。**注意**:此文件包含 OpenCV 头文件(`opencv2/core/types.hpp`),如需完全隐藏 OpenCV 依赖,建议使用包装器模式。 + + +## 一点个性 + +### ASCII 艺术 +- **[acsii_art.hpp](../src/utility/acsii_art.hpp)**: 提供 ASCII 艺术横幅,用于程序启动时显示 RMCS 标识。 + diff --git a/package.xml b/package.xml index f7e5b742..61f86b26 100644 --- a/package.xml +++ b/package.xml @@ -14,8 +14,9 @@ rmcs_executor + hikcamera ament_cmake - \ No newline at end of file + diff --git a/src/component.cpp b/src/component.cpp index 7cea0b7a..5c6d8f93 100644 --- a/src/component.cpp +++ b/src/component.cpp @@ -21,6 +21,21 @@ class AutoAimComponent final : public rmcs_executor::Component { } auto update() -> void override { + using namespace rmcs_description; + if (rmcs_tf.ready()) [[likely]] { + auto camera_odom = + fast_tf::lookup_transform( + *rmcs_tf); + + control_state.timestamp = Clock::now(); + + control_state.camera_to_odom_transform.posture = camera_odom.translation(); + control_state.camera_to_odom_transform.orientation = + Eigen::Quaterniond(camera_odom.rotation()); + + //... + } + recv_state(); send_state(); } @@ -33,6 +48,8 @@ class AutoAimComponent final : public rmcs_executor::Component { ControlClient::Send shm_send; ControlClient::Recv shm_recv; + ControlState control_state; + FramerateCounter framerate; private: @@ -46,6 +63,7 @@ class AutoAimComponent final : public rmcs_executor::Component { if (shm_recv.is_updated()) { auto timestamp = Stamp {}; + shm_recv.with_read([&](const auto& state) { timestamp = state.timestamp; }); if (shm_recv.is_updated()) { @@ -67,8 +85,9 @@ class AutoAimComponent final : public rmcs_executor::Component { return; } - shm_send.with_write([](ControlState& state) { - state.timestamp = Clock::now(); + shm_send.with_write([&](ControlState& state) { + state = control_state; + // ... }); } diff --git a/src/kernel/pose_estimator.cpp b/src/kernel/pose_estimator.cpp index cf4558d3..90d93efd 100644 --- a/src/kernel/pose_estimator.cpp +++ b/src/kernel/pose_estimator.cpp @@ -1,7 +1,9 @@ #include "pose_estimator.hpp" + #include "kernel/transform_tree.hpp" #include "utility/logging/printer.hpp" #include "utility/math/solve_pnp.hpp" +#include "utility/math/solve_pnp/pnp_solution.hpp" #include "utility/serializable.hpp" #include "utility/yaml/tf.hpp" @@ -16,25 +18,24 @@ struct PoseEstimator::Impl { std::array camera_matrix; std::array distort_coeff; - // clang-format off constexpr static std::tuple metas { - &Config::camera_matrix, "camera_matrix", - &Config::distort_coeff, "distort_coeff", + &Config::camera_matrix, + "camera_matrix", + &Config::distort_coeff, + "distort_coeff", }; - // clang-format on }; + Config config; + PnpSolution pnp_solution {}; Printer log { "PoseEstimator" }; - PnpSolution pnp_solution; - auto initialize(const YAML::Node& yaml) noexcept -> std::expected try { auto result = config.serialize(yaml); if (!result.has_value()) { return std::unexpected { result.error() }; } - { auto result = serialize_from(yaml["transforms"]); if (!result.has_value() @@ -43,13 +44,61 @@ struct PoseEstimator::Impl { + util::to_string(result.error()) }; } } + { + pnp_solution.input.camera_matrix = + reshape_array(config.camera_matrix); + pnp_solution.input.distort_coeff = + reshape_array(config.distort_coeff); + } return {}; } catch (const std::exception& e) { return std::unexpected { e.what() }; } - auto transform() { } + auto solve_pnp(std::optional> const& armors) noexcept + -> std::optional> { + if (!armors.has_value()) return std::nullopt; + auto const& _armors = *armors; + + auto armor_shape = [](ArmorShape shape) { + if (shape == ArmorShape::SMALL) { + return rmcs::kSmallArmorShapeOpenCV; + } else { + return rmcs::kLargeArmorShapeOpenCV; + } + }; + + auto armors_in_camera = std::vector {}; + + std::ranges::for_each(_armors | std::views::enumerate, + [&armors_in_camera, &armor_shape, this](auto const& item) { + auto [i, armor] = item; + + pnp_solution.input.armor_shape = armor_shape(armor.shape); + pnp_solution.input.genre = armor.genre; + pnp_solution.input.color = armor_color2camp_color(armor.color); + std::ranges::copy(armor.corners(), pnp_solution.input.armor_detection.begin()); + + auto solved = pnp_solution.solve(); + if (!solved) { + log.warn("solvePnP failed for armor {} ({} {})", i, + get_enum_name(armor.genre), get_enum_name(armor.color)); + return; + } + + auto armor_3d = Armor3D {}; + armor_3d.genre = pnp_solution.result.genre; + armor_3d.color = camp_color2armor_color(pnp_solution.result.color); + armor_3d.id = i; + pnp_solution.result.translation.copy_to(armor_3d.translation); + pnp_solution.result.orientation.copy_to(armor_3d.orientation); + + armors_in_camera.emplace_back(armor_3d); + }); + + return armors_in_camera; + } }; auto PoseEstimator::initialize(const YAML::Node& yaml) noexcept @@ -57,6 +106,11 @@ auto PoseEstimator::initialize(const YAML::Node& yaml) noexcept return pimpl->initialize(yaml); } +auto PoseEstimator::solve_pnp(std::optional> const& armors) const noexcept + -> std::optional> { + return pimpl->solve_pnp(armors); +} + PoseEstimator::PoseEstimator() noexcept : pimpl { std::make_unique() } { } diff --git a/src/kernel/pose_estimator.hpp b/src/kernel/pose_estimator.hpp index 2c30f633..76f20e49 100644 --- a/src/kernel/pose_estimator.hpp +++ b/src/kernel/pose_estimator.hpp @@ -1,6 +1,9 @@ #pragma once + #include "utility/math/linear.hpp" #include "utility/pimpl.hpp" +#include "utility/rclcpp/node.hpp" +#include "utility/robot/armor.hpp" #include #include @@ -10,8 +13,14 @@ class PoseEstimator { RMCS_PIMPL_DEFINITION(PoseEstimator) public: + using RclcppNode = util::RclcppNode; + auto initialize(const YAML::Node&) noexcept -> std::expected; + auto visualize(RclcppNode& visual_node) -> void; + + auto solve_pnp(std::optional> const&) const noexcept + -> std::optional>; auto update_imu_link(const Orientation&) noexcept -> void; }; diff --git a/src/kernel/visualization.cpp b/src/kernel/visualization.cpp index 297bc629..f10b8d08 100644 --- a/src/kernel/visualization.cpp +++ b/src/kernel/visualization.cpp @@ -1,13 +1,15 @@ #include "visualization.hpp" -#include "module/debug/visualization/stream_session.hpp" +#include + +#include "module/debug/visualization/armor_visualizer.hpp" +#include "module/debug/visualization/stream_session.hpp" #include "utility/image/image.details.hpp" #include "utility/logging/printer.hpp" #include "utility/serializable.hpp" -#include - using namespace rmcs::kernel; +using namespace rmcs::util; constexpr std::array kVideoTypes { "RTP_JEPG", @@ -47,9 +49,14 @@ struct Visualization::Impl { bool is_initialized = false; bool size_determined = false; - Impl() noexcept { session = std::make_unique(); } + std::unique_ptr armor_visualizer; + + Impl() noexcept { + session = std::make_unique(); + armor_visualizer = std::make_unique(); + } - auto initialize(const YAML::Node& yaml) noexcept -> NormalResult { + auto initialize(const YAML::Node& yaml, RclcppNode& visual_node) noexcept -> NormalResult { auto config = Config {}; auto result = config.serialize(yaml); if (!result.has_value()) { @@ -67,6 +74,9 @@ struct Visualization::Impl { } else { return std::unexpected { "Unknown video type: " + config.stream_type }; } + + armor_visualizer->initialize(visual_node); + is_initialized = true; return {}; } @@ -114,11 +124,15 @@ struct Visualization::Impl { return session->push_frame(mat); } + auto visualize_armors(std::span const& armors) const -> bool { + if (!is_initialized) return false; + return armor_visualizer->visualize(armors); + } }; -auto Visualization::initialize(const YAML::Node& yaml) noexcept +auto Visualization::initialize(const YAML::Node& yaml, RclcppNode& visual_node) noexcept -> std::expected { - return pimpl->initialize(yaml); + return pimpl->initialize(yaml, visual_node); } auto Visualization::initialized() const noexcept -> bool { return pimpl->initialized(); } @@ -127,6 +141,10 @@ auto Visualization::send_image(const Image& image) noexcept -> bool { return pimpl->send_image(image); } +auto Visualization::visualize_armors(std::span const& armors) const -> bool { + return pimpl->visualize_armors(armors); +} + Visualization::Visualization() noexcept : pimpl { std::make_unique() } { } diff --git a/src/kernel/visualization.hpp b/src/kernel/visualization.hpp index 419e2a97..efd00d9b 100644 --- a/src/kernel/visualization.hpp +++ b/src/kernel/visualization.hpp @@ -1,5 +1,7 @@ #pragma once #include "utility/image/image.hpp" +#include "utility/rclcpp/node.hpp" +#include "utility/robot/armor.hpp" #include #include @@ -17,11 +19,14 @@ class Visualization { } public: - auto initialize(const YAML::Node&) noexcept -> std::expected; + auto initialize(const YAML::Node& yaml, util::RclcppNode& visual_node) noexcept + -> std::expected; auto initialized() const noexcept -> bool; auto send_image(const Image&) noexcept -> bool; + + auto visualize_armors(std::span const& armors) const -> bool; }; } diff --git a/src/module/capturer/local_video.cpp b/src/module/capturer/local_video.cpp index 64bc1bf7..af63632c 100644 --- a/src/module/capturer/local_video.cpp +++ b/src/module/capturer/local_video.cpp @@ -1,16 +1,126 @@ #include "local_video.hpp" + +#include +#include + +#include + +#include "utility/image/image.details.hpp" + using namespace rmcs::cap; -struct LocalVideo::Impl { }; +struct LocalVideo::Impl { + Config config; + + using Clock = std::chrono::steady_clock; + + std::optional capturer; + + std::chrono::nanoseconds interval_duration { 0 }; + Clock::time_point last_read_time { Clock::now() }; + + auto set_framerate_interval(double hz) noexcept -> void { + if (hz > 0) { + interval_duration = + std::chrono::nanoseconds(static_cast(std::round(1.0 / hz * 1e9))); + } else { + interval_duration = std::chrono::nanoseconds { 0 }; + } + }; + + auto configure(Config const& _config) -> std::expected { + if (_config.location.empty() || !std::filesystem::exists(_config.location)) { + return std::unexpected { "Local video is not found or location is empty" }; + } + + config = _config; + + try { + capturer.emplace(config.location); + } catch (std::exception const& e) { + return std::unexpected { "Failed to construct VideoCapture: " + std::string(e.what()) }; + } catch (...) { + return std::unexpected { "Failed to construct VideoCapture due to an unknown error." }; + } + + double source_fps = capturer->get(cv::CAP_PROP_FPS); + double target_fps = source_fps > 0 ? source_fps : 30.0; + + if (config.frame_rate > 0) { + target_fps = config.frame_rate; + } + + set_framerate_interval(target_fps); + + last_read_time = Clock::now(); + + return {}; + } + + auto connect() -> std::expected { return configure(config); } + + auto connected() const noexcept -> bool { return capturer.has_value() && capturer->isOpened(); } + + auto disconnect() noexcept -> void { + if (capturer.has_value()) { + capturer.reset(); + } + interval_duration = std::chrono::nanoseconds { 0 }; + } + + auto wait_image() noexcept -> std::expected, std::string> { + if (!capturer.has_value() || !capturer->isOpened()) { + return std::unexpected { "Video stream is not opened." }; + } + + const auto time_before_read = Clock::now(); + const auto next_read_time_expected = last_read_time + interval_duration; + auto wait_duration = next_read_time_expected - time_before_read; + + if (wait_duration.count() > 0) { + std::this_thread::sleep_for(wait_duration); + last_read_time = next_read_time_expected; + } else { + last_read_time = config.allow_skipping ? Clock::now() : next_read_time_expected; + } + + auto frame = cv::Mat {}; + auto image = std::make_unique(); + if (!capturer->read(frame)) { + if (config.loop_play) { + if (capturer->set(cv::CAP_PROP_POS_FRAMES, 0) && capturer->read(frame)) { + last_read_time = Clock::now(); + } else { + return std::unexpected { "End of file reached and failed to " + "loop/reset." }; + } + } else { + return std::unexpected { "End of file reached." }; + } + } + + if (frame.empty()) { + return std::unexpected { "Read frame is empty, possibly due to IO error." }; + } + image->details().set_mat(frame); + image->set_timestamp(last_read_time); + + return image; + }; +}; -auto LocalVideo::configure(const ConfigDetail& config) noexcept - -> std::expected { } +auto LocalVideo::configure(Config const& config) -> std::expected { + return pimpl->configure(config); +} +auto LocalVideo::wait_image() noexcept -> std::expected, std::string> { + return pimpl->wait_image(); +} -auto LocalVideo::connect() noexcept -> std::expected { } +auto LocalVideo::connect() noexcept -> std::expected { return pimpl->connect(); } -auto LocalVideo::connected() const noexcept -> bool { } +auto LocalVideo::connected() const noexcept -> bool { return pimpl->connected(); } -auto LocalVideo::wait_image() -> std::expected, std::string> { } +auto LocalVideo::disconnect() noexcept -> void { return pimpl->disconnect(); } LocalVideo::LocalVideo() noexcept : pimpl { std::make_unique() } { } diff --git a/src/module/capturer/local_video.hpp b/src/module/capturer/local_video.hpp index 69ae1818..bf7515cf 100644 --- a/src/module/capturer/local_video.hpp +++ b/src/module/capturer/local_video.hpp @@ -6,30 +6,39 @@ namespace rmcs::cap { -class LocalVideo { +struct LocalVideo { RMCS_PIMPL_DEFINITION(LocalVideo) -public: +private: struct ConfigDetail { std::string location; + double frame_rate; + bool loop_play; + bool allow_skipping; }; +public: struct Config : ConfigDetail, util::Serializable { constexpr static std::tuple metas { - &Config::location, - "location", + &ConfigDetail::location, + "location", // 视频文件路径 + &ConfigDetail::frame_rate, + "frame_rate", // 帧率 + &ConfigDetail::loop_play, + "loop_play", // 循环播放 + &ConfigDetail::allow_skipping, + "allow_skipping" // 允许跳帧以保证实时性 }; }; - auto configure(const ConfigDetail&) noexcept -> std::expected; + auto configure(Config const&) -> std::expected; auto connect() noexcept -> std::expected; - auto disconnect() noexcept { } - auto connected() const noexcept -> bool; - auto wait_image() -> std::expected, std::string>; -}; + auto disconnect() noexcept -> void; + auto wait_image() noexcept -> std::expected, std::string>; +}; } diff --git a/src/module/debug/visualization/armor_visualizer.cpp b/src/module/debug/visualization/armor_visualizer.cpp new file mode 100644 index 00000000..afc95912 --- /dev/null +++ b/src/module/debug/visualization/armor_visualizer.cpp @@ -0,0 +1,90 @@ +#include "armor_visualizer.hpp" + +#include "utility/rclcpp/visual/armor.hpp" +#include "utility/robot/armor.hpp" + +using namespace rmcs::debug; +using VisualArmor = rmcs::util::visual::Armor; + +struct ArmorShadow { + decltype(rmcs::Armor3D::genre) genre; + decltype(rmcs::Armor3D::color) color; + decltype(rmcs::Armor3D::id) id; + + bool operator==(ArmorShadow const& other) const = default; + bool operator!=(ArmorShadow const& other) const { return !(*this == other); } +}; + +struct ArmorVisualizer::Impl final { + auto initialize(util::RclcppNode& visual_node) noexcept -> void { + node = std::ref(visual_node); + } + + auto visualize(std::span const& _armors) -> bool { + if (!node.has_value()) { + return false; + } + + auto new_size = _armors.size(); + visual_armors.reserve(new_size); + current_armors.reserve(new_size); + visual_armors.resize(new_size); + current_armors.resize(new_size); + + for (size_t i = 0; i < new_size; i++) { + auto const& input = _armors[i]; + auto& armor_ptr = visual_armors[i]; + auto& shadow = current_armors[i]; + + bool changed = !armor_ptr || needs_rebuild(shadow, input); + + if (changed) { + auto const config = VisualArmor::Config { + .rclcpp = node.value().get(), + .device = input.genre, + .camp = armor_color2camp_color(input.color), + .id = input.id, + .name = "solved_pnp_armor", + .tf = "camera_link", + }; + + armor_ptr = std::make_unique(config); + + shadow.genre = input.genre; + shadow.color = input.color; + shadow.id = input.id; + } + + armor_ptr->move(input.translation, input.orientation); + armor_ptr->update(); + } + + return true; + } + + // static auto camp(ArmorColor const& color) -> CampColor { + // if (color == ArmorColor::BLUE) return CampColor::BLUE; + // if (color == ArmorColor::RED) return CampColor::RED; + // return CampColor::UNKNOWN; + // }; + + static bool needs_rebuild(ArmorShadow shadow, Armor3D const& input) { + return shadow.genre != input.genre || shadow.color != input.color || shadow.id != input.id; + } + + std::optional> node; + std::vector current_armors; + std::vector> visual_armors; +}; + +auto ArmorVisualizer::initialize(util::RclcppNode& visual_node) noexcept -> void { + return pimpl->initialize(visual_node); +} + +auto ArmorVisualizer::visualize(std::span const& armors) -> bool { + return pimpl->visualize(armors); +} + +ArmorVisualizer::ArmorVisualizer() noexcept + : pimpl { std::make_unique() } { }; +ArmorVisualizer::~ArmorVisualizer() noexcept = default; diff --git a/src/module/debug/visualization/armor_visualizer.hpp b/src/module/debug/visualization/armor_visualizer.hpp new file mode 100644 index 00000000..bcc8bea4 --- /dev/null +++ b/src/module/debug/visualization/armor_visualizer.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "utility/pimpl.hpp" +#include "utility/rclcpp/node.hpp" +#include "utility/robot/armor.hpp" + +namespace rmcs::debug { + +class ArmorVisualizer { + + RMCS_PIMPL_DEFINITION(ArmorVisualizer) + +public: + auto initialize(util::RclcppNode&) noexcept -> void; + + auto visualize(std::span const&) -> bool; +}; +} diff --git a/src/module/identifier/model.cpp b/src/module/identifier/model.cpp index dff83fcc..285ecddc 100644 --- a/src/module/identifier/model.cpp +++ b/src/module/identifier/model.cpp @@ -234,6 +234,7 @@ struct OpenVinoNet::Impl { for (std::size_t row = 0; row < rows; row++) { auto line = armor_type {}; + //(0,1) 顶左,(2,3) 底左,(4,5) 底右,(6,7) 顶右 line.unsafe_from(std::span { data + row * cols, cols }); line.confidence = util::sigmoid(line.confidence); diff --git a/src/runtime.cpp b/src/runtime.cpp index a1f53770..4ece5e70 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -23,6 +23,7 @@ auto main() -> int { std::signal(SIGINT, [](int) { util::set_running(false); }); auto rclcpp_node = util::RclcppNode { "AutoAim" }; + rclcpp_node.set_pub_topic_prefix("/rmcs/auto_aim/"); auto handle_result = [&](auto runtime_name, const auto& result) { if (!result.has_value()) { @@ -60,9 +61,9 @@ auto main() -> int { { auto config = configuration["identifier"]; - const auto path = std::filesystem::path { util::Parameters::share_location() } + const auto model_location = std::filesystem::path { util::Parameters::share_location() } / std::filesystem::path { config["model_location"].as() }; - config["model_location"] = path.string(); + config["model_location"] = model_location.string(); auto result = identifier.initialize(config); handle_result("identifier", result); @@ -76,7 +77,7 @@ auto main() -> int { // VISUALIZATION if (use_visualization) { auto config = configuration["visualization"]; - auto result = visualization.initialize(config); + auto result = visualization.initialize(config, rclcpp_node); handle_result("visualization", result); } @@ -95,26 +96,29 @@ auto main() -> int { for (const auto& armor_2d : *armors_2d) util::draw(*image, armor_2d); } + if (visualization.initialized()) { visualization.send_image(*image); } - auto armor_3d = std::ignore; - - auto future_state = std::ignore; - using namespace rmcs::util; + control_system.update_state({ .timestamp = Clock::now(), }); - if (framerate.tick()) { - rclcpp_node.info("Framerate: {}hz", framerate.fps()); + auto armors_3d = pose_estimator.solve_pnp(armors_2d); + + if (!armors_3d.has_value()) continue; + + if (visualization.initialized()) { + visualization.visualize_armors(*armors_3d); } + // TODO: pose estimator + // TODO: predictor + // TODO: control + rclcpp_node.spin_once(); } - - rclcpp_node.spin_once(); } - rclcpp_node.shutdown(); } diff --git a/src/utility/image/armor.cpp b/src/utility/image/armor.cpp index 3472f1de..876e5978 100644 --- a/src/utility/image/armor.cpp +++ b/src/utility/image/armor.cpp @@ -47,6 +47,7 @@ auto draw(Image& canvas, const Armor2D& armor) noexcept -> void { cv::putText(opencv_mat, "TR", armor.tr, font, scale, white, thickness, cv::LINE_AA); cv::putText(opencv_mat, "BL", armor.bl, font, scale, white, thickness, cv::LINE_AA); cv::putText(opencv_mat, "BR", armor.br, font, scale, white, thickness, cv::LINE_AA); + cv::putText(opencv_mat, "TL", armor.tl, font, scale, white, thickness, cv::LINE_AA); auto info = std::format("{:.2f} {} {}", armor.confidence, genre, shape); cv::putText(opencv_mat, info, armor.tl + cv::Point2f { 0, -5 }, font, scale, white, thickness, diff --git a/src/utility/math/conversion.hpp b/src/utility/math/conversion.hpp new file mode 100644 index 00000000..a00820b6 --- /dev/null +++ b/src/utility/math/conversion.hpp @@ -0,0 +1,35 @@ + +#pragma once + +#include + +namespace rmcs::util { + +// OpenCV 与 ROS 坐标系之间的变换矩阵 +static const Eigen::Matrix3d kCoordTransformMatrix = + // clang-format off + (Eigen::Matrix3d() << 0, 0, 1, + -1, 0, 0, + 0,-1, 0).finished(); +// clang-format on + +static inline auto opencv2ros_position(const Eigen::Vector3d& position) -> Eigen::Vector3d { + auto result = Eigen::Vector3d(position.z(), -position.x(), -position.y()); + return result; +} + +static inline Eigen::Matrix3d opencv2ros_rotation(const Eigen::Matrix3d& rotation_matrix) { + return kCoordTransformMatrix * rotation_matrix * kCoordTransformMatrix.transpose(); +} + +static inline Eigen::Vector3d ros2opencv_position(const Eigen::Vector3d& position) { + auto result = Eigen::Vector3d(-position.y(), -position.z(), position.x()); + return result; +} + +static inline Eigen::Matrix3d ros2opencv_rotation(const Eigen::Matrix3d& rotation_matrix) { + + return kCoordTransformMatrix.transpose() * rotation_matrix * kCoordTransformMatrix; +} + +} diff --git a/src/utility/math/solve_pnp.cpp b/src/utility/math/solve_pnp.cpp deleted file mode 100644 index 1f0e898b..00000000 --- a/src/utility/math/solve_pnp.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "solve_pnp.hpp" -#include -#include -#include -#include - -using namespace rmcs::util; - -template -static auto cast_opencv_matrix(std::array& source) { - auto mat_type = int {}; - /* */ if constexpr (std::same_as) { - mat_type = CV_64FC1; - } else if constexpr (std::same_as) { - mat_type = CV_32FC1; - } else if constexpr (std::same_as) { - mat_type = CV_32SC1; - } else { - static_assert(false, "Unsupport mat scale type"); - } - - return cv::Mat { 1, cols, mat_type, source.data() }; -} - -template -static auto cast_opencv_matrix(std::array, rows>& source) { - auto mat_type = int {}; - /* */ if constexpr (std::same_as) { - mat_type = CV_64FC1; - } else if constexpr (std::same_as) { - mat_type = CV_32FC1; - } else if constexpr (std::same_as) { - mat_type = CV_32SC1; - } else { - static_assert(false, "Unsupport mat scale type"); - } - - return cv::Mat { rows, cols, mat_type, source[0].data() }; -} - -auto PnpSolution::solve() noexcept -> void { - const auto camera_matrix = cast_opencv_matrix(input.camera_matrix); - const auto distort_coeff = cast_opencv_matrix(input.distort_coeff); - - const auto armor_shape = std::ranges::to(input.armor_shape - | std::views::transform([](const Point3d& point) { return point.make(); })); - - const auto armor_detection = std::ranges::to(input.armor_detection - | std::views::transform([](const Point2d& point) { return point.make(); })); - - auto rota_vec = cv::Vec3d {}; - auto tran_vec = cv::Vec3d {}; - cv::solvePnP(armor_shape, armor_detection, camera_matrix, distort_coeff, rota_vec, tran_vec, - false, cv::SOLVEPNP_IPPE); - - { - result.translation.x = tran_vec[0]; - result.translation.y = tran_vec[1]; - result.translation.z = tran_vec[2]; - } - { - auto rotation_opencv = cv::Mat {}; - cv::Rodrigues(rota_vec, rotation_opencv); - - auto rotation_eigen = Eigen::Matrix3d {}; - rotation_eigen << // Col Major - rotation_opencv.at(0, 0), // [0,0] - rotation_opencv.at(0, 1), // [0,1] - rotation_opencv.at(0, 2), // [0,2] - rotation_opencv.at(1, 0), // [1,0] - rotation_opencv.at(1, 1), // [1,1] - rotation_opencv.at(1, 2), // [1,2] - rotation_opencv.at(2, 0), // [2,0] - rotation_opencv.at(2, 1), // [2,1] - rotation_opencv.at(2, 2); // [2,2] - result.orientation = Eigen::Quaterniond { rotation_eigen }; - } -} diff --git a/src/utility/math/solve_pnp.hpp b/src/utility/math/solve_pnp.hpp index 806b098a..257626d6 100644 --- a/src/utility/math/solve_pnp.hpp +++ b/src/utility/math/solve_pnp.hpp @@ -1,26 +1,73 @@ #pragma once -#include "utility/math/linear.hpp" -#include "utility/math/point.hpp" -#include - -namespace rmcs::util { - -struct PnpSolution { - struct Input { - // Row Major - std::array, 3> camera_matrix; - std::array distort_coeff; - std::array armor_shape; - std::array armor_detection; - } input; - struct Result { - Translation translation; - Orientation orientation; - } result; - - PnpSolution() noexcept = default; - - auto solve() noexcept -> void; -}; +#include +#include +#include + +template +static auto cast_opencv_matrix(std::array& source) { + auto mat_type = int {}; + /* */ if constexpr (std::same_as) { + mat_type = CV_64FC1; + } else if constexpr (std::same_as) { + mat_type = CV_32FC1; + } else if constexpr (std::same_as) { + mat_type = CV_32SC1; + } else { + static_assert(false, "Unsupport mat scale type"); + } + + return cv::Mat { 1, cols, mat_type, source.data() }; +} + +template +static auto cast_opencv_matrix(std::array, rows>& source) { + auto mat_type = int {}; + /* */ if constexpr (std::same_as) { + mat_type = CV_64FC1; + } else if constexpr (std::same_as) { + mat_type = CV_32FC1; + } else if constexpr (std::same_as) { + mat_type = CV_32SC1; + } else { + static_assert(false, "Unsupport mat scale type"); + } + + return cv::Mat { rows, cols, mat_type, source[0].data() }; +} + +template +concept ConvertibleTo = std::is_convertible_v; + +/* @Note: Row-Major */ +template + requires ConvertibleTo +static auto reshape_array(std::array const& input_array) + -> std::array, rows> { + static_assert(N == rows * cols, "input_array的元素总数N必须等于rows*cols"); + using ResultArray = std::array, rows>; + + ResultArray result_array; + for (std::size_t i = 0; i < N; ++i) { + std::size_t row_idx = i / cols; + std::size_t col_idx = i % cols; + + result_array[row_idx][col_idx] = input_array[i]; + } + return result_array; +} + +/* @Note: Row-Major */ +template + requires ConvertibleTo +static auto reshape_array(std::array const& input_array) + -> std::array { + using ResultArray = std::array; + + ResultArray result_array; + std::transform(input_array.begin(), input_array.end(), result_array.begin(), + [](const input_type& val) { return static_cast(val); }); + + return result_array; } diff --git a/src/utility/math/solve_pnp/pnp_solution.cpp b/src/utility/math/solve_pnp/pnp_solution.cpp new file mode 100644 index 00000000..9b4c76ab --- /dev/null +++ b/src/utility/math/solve_pnp/pnp_solution.cpp @@ -0,0 +1,45 @@ +#include "pnp_solution.hpp" + +#include "utility/math/conversion.hpp" +#include "utility/math/solve_pnp.hpp" +#include +#include +#include + +#define OPENCV_DISABLE_EIGEN_TENSOR_SUPPORT +#include + +using namespace rmcs::util; +auto PnpSolution::solve() noexcept -> bool { + const auto camera_matrix = cast_opencv_matrix(input.camera_matrix); + const auto distort_coeff = cast_opencv_matrix(input.distort_coeff); + + const auto armor_shape = std::ranges::to(input.armor_shape + | std::views::transform([](const Point3d& point) { return point.make(); })); + + const auto armor_detection = std::ranges::to(input.armor_detection + | std::views::transform([](const Point2d& point) { return point.make(); })); + + auto rota_vec = cv::Vec3d {}; + auto tran_vec = cv::Vec3d {}; + auto success = cv::solvePnP(armor_shape, armor_detection, camera_matrix, distort_coeff, + rota_vec, tran_vec, false, cv::SOLVEPNP_IPPE); + + if (!success) return false; + + auto tran_vec_eigen_opencv = Eigen::Vector3d {}; + cv::cv2eigen(tran_vec, tran_vec_eigen_opencv); + + auto rotation_opencv = cv::Mat {}; + cv::Rodrigues(rota_vec, rotation_opencv); + auto rotation_eigen_opencv = Eigen::Matrix3d {}; + cv::cv2eigen(rotation_opencv, rotation_eigen_opencv); + + result.genre = input.genre; + result.color = input.color; + result.translation = opencv2ros_position(tran_vec_eigen_opencv); + result.orientation = + Eigen::Quaterniond(opencv2ros_rotation(rotation_eigen_opencv)).normalized(); + + return true; +} diff --git a/src/utility/math/solve_pnp/pnp_solution.hpp b/src/utility/math/solve_pnp/pnp_solution.hpp new file mode 100644 index 00000000..1499dcb0 --- /dev/null +++ b/src/utility/math/solve_pnp/pnp_solution.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "utility/math/linear.hpp" +#include "utility/math/point.hpp" +#include "utility/robot/color.hpp" +#include "utility/robot/id.hpp" + +namespace rmcs::util { + +struct PnpSolution { + struct Input { + // Row Major + std::array, 3> camera_matrix; + std::array distort_coeff; + std::array armor_shape; + std::array armor_detection; + DeviceId genre; + CampColor color; + } input; + struct Result { + Translation translation; + Orientation orientation; + DeviceId genre; + CampColor color; + } result; + + PnpSolution() noexcept = default; + + auto solve() noexcept -> bool; +}; +} diff --git a/src/utility/model/armor_detection.hpp b/src/utility/model/armor_detection.hpp index 7c6ccd95..c2c7c917 100644 --- a/src/utility/model/armor_detection.hpp +++ b/src/utility/model/armor_detection.hpp @@ -9,15 +9,16 @@ struct ArmorInferResult { using Point = cv::Point_; using Rect = cv::Rect_; + //(0,1) 顶左,(2,3) 底左,(4,5) 底右,(6,7) 顶右 struct Corners { precision_type lt_x; precision_type lt_y; + precision_type lb_x; + precision_type lb_y; precision_type rb_x; precision_type rb_y; precision_type rt_x; precision_type rt_y; - precision_type lb_x; - precision_type lb_y; auto lt() const noexcept { return Point { lt_x, lt_y }; } auto rb() const noexcept { return Point { rb_x, rb_y }; } auto rt() const noexcept { return Point { rt_x, rt_y }; } diff --git a/src/utility/rclcpp/visual/armor.cpp b/src/utility/rclcpp/visual/armor.cpp index f60be4bd..03f71020 100644 --- a/src/utility/rclcpp/visual/armor.cpp +++ b/src/utility/rclcpp/visual/armor.cpp @@ -1,59 +1,106 @@ #include "armor.hpp" #include "utility/panic.hpp" #include "utility/rclcpp/node.details.hpp" -#include -#include + +#include using namespace rmcs::util::visual; -using Marker = visualization_msgs::msg::Marker; +using Marker = visualization_msgs::msg::Marker; +using MarkerArray = visualization_msgs::msg::MarkerArray; struct Armor::Impl { static inline rclcpp::Clock rclcpp_clock { RCL_SYSTEM_TIME }; + std::unique_ptr config; + Marker marker; - std::shared_ptr> rclcpp_pub; + Marker arrow_marker; + std::shared_ptr> rclcpp_pub; - explicit Impl(const Config& config) { - auto& rclcpp = config.rclcpp; - auto& details = config.rclcpp.details; + explicit Impl(const Config& config) + : config(std::make_unique(config)) { + initialize(); + } - if (!prefix::check_naming(config.id) || !prefix::check_naming(config.tf)) { - util::panic( - std::format("Not a valid naming for armor id or tf: {}", prefix::naming_standard)); - } + static auto create_rclcpp_publisher(Config const& config) + -> std::shared_ptr> { + const auto topic_name { config.rclcpp.get_pub_topic_prefix() + config.name }; - const auto topic_name { rclcpp.get_pub_topic_prefix() + config.id }; - rclcpp_pub = details->make_pub(topic_name, qos::debug); + if (!config.rclcpp.details) + util::panic("Rclcpp node details are required in config to create publisher."); - marker.header.frame_id = config.tf; + return config.rclcpp.details->make_pub(topic_name, qos::debug); + } + + auto initialize() -> void { + if (!prefix::check_naming(config->name) || !prefix::check_naming(config->tf)) { + util::panic(std::format( + "Not a valid naming for armor name or tf: {}", prefix::naming_standard)); + } - marker.ns = config.id; - marker.id = 0; - marker.type = Marker::CUBE; - marker.action = Marker::ADD; + marker.header.frame_id = config->tf; + marker.ns = config->name; + marker.id = config->id; + marker.type = Marker::CUBE; + marker.action = Marker::ADD; + marker.lifetime = rclcpp::Duration::from_seconds(0.1); // ref: "https://www.robomaster.com/zh-CN/products/components/detail/149" - /* */ if (DeviceIds::kSmallArmorDevices().contains(config.device)) { + /* */ if (DeviceIds::kSmallArmorDevices().contains(config->device)) { marker.scale.x = 0.003, marker.scale.y = 0.140, marker.scale.z = 0.125; - } else if (DeviceIds::kLargeArmorDevices().contains(config.device)) { + } else if (DeviceIds::kLargeArmorDevices().contains(config->device)) { marker.scale.x = 0.003, marker.scale.y = 0.235, marker.scale.z = 0.127; } else { util::panic("Wrong device id for a visualized armor"); }; - /* */ if (config.camp == CampColor::RED) { + /* */ if (config->camp == CampColor::RED) { marker.color.r = 1., marker.color.g = 0., marker.color.b = 0., marker.color.a = 1.; - } else if (config.camp == CampColor::BLUE) { + } else if (config->camp == CampColor::BLUE) { marker.color.r = 0., marker.color.g = 0., marker.color.b = 1., marker.color.a = 1.; } else { - util::panic("Please specify a valid armor color"); + marker.color.r = 1., marker.color.g = 0., marker.color.b = 1., marker.color.a = 1.; + } + + arrow_marker.header.frame_id = config->tf; + arrow_marker.ns = config->name + std::string("_arrow"); + arrow_marker.id = config->id; + arrow_marker.type = Marker::ARROW; + arrow_marker.action = Marker::ADD; + arrow_marker.lifetime = rclcpp::Duration::from_seconds(0.1); + + arrow_marker.scale.x = 0.2; + arrow_marker.scale.y = 0.01; + arrow_marker.scale.z = 0.01; + + /* */ if (config->camp == CampColor::RED) { + arrow_marker.color.r = 1., arrow_marker.color.g = 0., arrow_marker.color.b = 0., + arrow_marker.color.a = 1.; + } else if (config->camp == CampColor::BLUE) { + arrow_marker.color.r = 0., arrow_marker.color.g = 0., arrow_marker.color.b = 1., + arrow_marker.color.a = 1.; + } else { + arrow_marker.color.r = 1., arrow_marker.color.g = 0., arrow_marker.color.b = 1., + arrow_marker.color.a = 1.; } } auto update() noexcept -> void { - marker.header.stamp = rclcpp_clock.now(); - rclcpp_pub->publish(marker); + if (!rclcpp_pub) { + rclcpp_pub = create_rclcpp_publisher(*config); + } + + MarkerArray visual_marker; + const auto current_stamp = rclcpp_clock.now(); + marker.header.stamp = current_stamp; + arrow_marker.header.stamp = current_stamp; + + arrow_marker.pose = marker.pose; + visual_marker.markers.emplace_back(marker); + visual_marker.markers.emplace_back(arrow_marker); + + rclcpp_pub->publish(visual_marker); } auto move(const Translation& t, const Orientation& q) noexcept { diff --git a/src/utility/rclcpp/visual/armor.hpp b/src/utility/rclcpp/visual/armor.hpp index 5b6bf36a..e09f1a4f 100644 --- a/src/utility/rclcpp/visual/armor.hpp +++ b/src/utility/rclcpp/visual/armor.hpp @@ -3,6 +3,7 @@ #include "utility/rclcpp/visual/movable.hpp" #include "utility/robot/color.hpp" #include "utility/robot/id.hpp" +#include namespace rmcs::util::visual { @@ -14,11 +15,13 @@ struct Armor : public Movable { DeviceId device; CampColor camp; - std::string id; + int id; + std::string name; std::string tf; }; explicit Armor(const Config&) noexcept; + ~Armor() noexcept; Armor(const Armor&) = delete; diff --git a/src/utility/robot/armor.hpp b/src/utility/robot/armor.hpp index 7d7aaed7..a2bd7114 100644 --- a/src/utility/robot/armor.hpp +++ b/src/utility/robot/armor.hpp @@ -1,6 +1,8 @@ #pragma once #include "utility/math/point.hpp" +#include "utility/robot/color.hpp" #include "utility/robot/id.hpp" +#include #include #include @@ -11,6 +13,17 @@ constexpr auto get_enum_name(ArmorColor color) noexcept { constexpr std::array details { "DARK", "RED", "BLUE", "MIX" }; return details[std::to_underlying(color)]; } +inline constexpr auto armor_color2camp_color(ArmorColor const& color) -> CampColor { + if (color == ArmorColor::BLUE) return CampColor::BLUE; + if (color == ArmorColor::RED) return CampColor::RED; + return CampColor::UNKNOWN; +}; + +inline constexpr auto camp_color2armor_color(CampColor const& color) -> ArmorColor { + if (color == CampColor::BLUE) return ArmorColor::BLUE; + if (color == CampColor::RED) return ArmorColor::RED; + return ArmorColor::MIX; +}; enum class ArmorShape : bool { LARGE, SMALL }; constexpr auto get_enum_name(ArmorShape shape) noexcept { @@ -43,22 +56,46 @@ struct Armor2D { } }; -struct Armor3D { }; +struct Armor3D { + ArmorGenre genre; + ArmorColor color; + int id; + + Eigen::Vector3d translation; + Eigen::Quaterniond orientation; +}; struct Armor { }; using Armors = std::vector; -constexpr std::array kLargeArmorShape { +constexpr double kLightBarHeight = 0.056; +constexpr double kLargeArmorWidth = 0.23; +constexpr double kSmallArmorWidth = 0.135; + +constexpr std::array kLargeArmorShapeOpenCV { + Point3d { -0.5 * kLargeArmorWidth, -0.5 * kLightBarHeight, 0.0 }, // Top-left + Point3d { 0.5 * kLargeArmorWidth, -0.5 * kLightBarHeight, 0.0 }, // Top-right + Point3d { 0.5 * kLargeArmorWidth, 0.5 * kLightBarHeight, -0.0 }, // Bottom-right + Point3d { -0.5 * kLargeArmorWidth, 0.5 * kLightBarHeight, -0.0 } // Bottom-left +}; + +constexpr std::array kSmallArmorShapeOpenCV { + Point3d { -0.5 * kSmallArmorWidth, -0.5 * kLightBarHeight, 0.0 }, // Top-left + Point3d { 0.5 * kSmallArmorWidth, -0.5 * kLightBarHeight, 0.0 }, // Top-right + Point3d { 0.5 * kSmallArmorWidth, 0.5 * kLightBarHeight, -0.0 }, // Bottom-right + Point3d { -0.5 * kSmallArmorWidth, 0.5 * kLightBarHeight, -0.0 } // Bottom-left +}; + +constexpr std::array kLargeArmorShapeRos { Point3d { 0.0, 0.115, 0.028 }, // Top-left - Point3d { 0.0, -0.115, 0.028 }, // Top-right Point3d { 0.0, -0.115, -0.028 }, // Bottom-right + Point3d { 0.0, -0.115, 0.028 }, // Top-right Point3d { 0.0, 0.115, -0.028 } // Bottom-left }; -constexpr std::array kSmallArmorShape { +constexpr std::array kSmallArmorShapeRos { Point3d { 0.0, 0.0675, 0.028 }, // Top-left - Point3d { 0.0, -0.0675, 0.028 }, // Top-right Point3d { 0.0, -0.0675, -0.028 }, // Bottom-right + Point3d { 0.0, -0.0675, 0.028 }, // Top-right Point3d { 0.0, 0.0675, -0.028 } // Bottom-left }; - } diff --git a/src/utility/robot/id.hpp b/src/utility/robot/id.hpp index 5ca91c67..9452c450 100644 --- a/src/utility/robot/id.hpp +++ b/src/utility/robot/id.hpp @@ -86,8 +86,9 @@ struct DeviceIds { static constexpr auto None() { return DeviceIds {}; } static constexpr auto Full() { return DeviceIds { (1 << 11) - 1 }; } - constexpr DeviceIds() = default; - constexpr DeviceIds(const DeviceIds&) = default; + constexpr DeviceIds() = default; + constexpr DeviceIds(const DeviceIds&) = default; + constexpr DeviceIds& operator=(const DeviceIds&) = default; constexpr explicit DeviceIds(uint16_t data) noexcept : data { data } { }; diff --git a/src/utility/shared/context.hpp b/src/utility/shared/context.hpp index 8b615b4b..3152ad47 100644 --- a/src/utility/shared/context.hpp +++ b/src/utility/shared/context.hpp @@ -16,6 +16,11 @@ enum class ShootMode { BUFF_LARGE, }; +struct Transform { + Direction3d posture {}; + Orientation orientation {}; +}; + struct AutoAimState { Stamp timestamp {}; @@ -34,6 +39,13 @@ struct ControlState { double bullet_speed {}; Orientation imu_state {}; + /*Note: + * 对应关系: + * odom<->fast_tf::OdomImu, + * camera<->fast_tf::CameraLink + * */ + Transform camera_to_odom_transform {}; + DeviceIds targets { DeviceIds::Full() }; }; static_assert(std::is_trivially_copyable_v); diff --git a/src/utility/tf/static_tf.hpp b/src/utility/tf/static_tf.hpp index a02b406d..f46f2683 100644 --- a/src/utility/tf/static_tf.hpp +++ b/src/utility/tf/static_tf.hpp @@ -94,9 +94,6 @@ struct Joint { using Result = typename FindInTuple::Result; }; - /// Path - /// TODO: - /// /// Function Based /// @@ -127,7 +124,7 @@ struct Joint { static constexpr auto find() noexcept { using Result = typename Find::Result; static_assert(!std::same_as, "没有找到你想要的变换节点"); - return Result { }; + return Result {}; } template diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 782f3577..dd33da70 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -68,7 +68,7 @@ target_link_libraries( test_model_infer yaml-cpp::yaml-cpp openvino::runtime - ${OpenCV_LIBS} + ${OpenCV_LIBRARIES} ) # Device Id @@ -81,11 +81,21 @@ ament_add_gtest( ament_add_gtest( test_solve_pnp ${TEST_DIR}/solve_pnp.cpp - ${RMCS_SRC_DIR}/utility/math/solve_pnp.cpp + ${RMCS_SRC_DIR}/utility/math/solve_pnp/pnp_solution.cpp + ${RMCS_SRC_DIR}/module/identifier/model.cpp + ${RMCS_SRC_DIR}/utility/image/image.cpp +) +ament_target_dependencies( + test_solve_pnp + rclcpp + visualization_msgs + geometry_msgs ) target_link_libraries( test_solve_pnp ${OpenCV_LIBRARIES} + openvino::runtime + yaml-cpp::yaml-cpp ) # Static TF @@ -98,3 +108,41 @@ target_link_libraries( test_static_tf yaml-cpp::yaml-cpp ) + +# Transform Communication +ament_add_gtest( + test_transform_communication + ${TEST_DIR}/transform_communication.cpp +) + +if(HIKCAMERA_AVAILABLE) + # Hikcamera + add_executable( + example_hikcamera + ${TEST_DIR}/hikcamera.cpp + ) + target_link_libraries( + example_hikcamera + ${OpenCV_LIBRARIES} + ${hikcamera_LIBRARIES} + ) + + # RTP Streaming + add_executable( + example_streaming + ${TEST_DIR}/streaming.cpp + ${RMCS_SRC_DIR}/module/capturer/hikcamera.cpp + ${RMCS_SRC_DIR}/module/debug/visualization/stream_session.cpp + ${RMCS_SRC_DIR}/module/debug/visualization/stream_context.cpp + ${RMCS_SRC_DIR}/utility/image/image.cpp + ${RMCS_SRC_DIR}/utility/rclcpp/node.cpp + ) + target_link_libraries( + example_streaming + rclcpp::rclcpp + ${OpenCV_LIBRARIES} + ${hikcamera_LIBRARIES} + ) +else() + message(STATUS "hikcamera 未检测到,跳过相关构建") +endif() diff --git a/test/asset.yml b/test/asset.yml index 3259ccac..c7e5578c 100644 --- a/test/asset.yml +++ b/test/asset.yml @@ -1,3 +1,13 @@ -b4_front_2_armors: https://pub-997cd3005edc4b9db91df913907990bf.r2.dev/autoaim/model_infer_example.jpg -b1_orientation: https://pub-997cd3005edc4b9db91df913907990bf.r2.dev/autoaim/orientation.mp4 -b1_translation: https://pub-997cd3005edc4b9db91df913907990bf.r2.dev/autoaim/translation.mp4 +resources: + #model_infer + model_infer_img: https://pub-997cd3005edc4b9db91df913907990bf.r2.dev/autoaim/model_infer_example.jpg + + #solve_pnp + blue_0_5m_0deg: "https://heyeuuu19.com/auto_aim/pnp/blue-0.5m.jpg" + blue_0_5m_45deg: "https://heyeuuu19.com/auto_aim/pnp/blue-0.5m-45degree.jpg" + blue_1_0m_0deg: "https://heyeuuu19.com/auto_aim/pnp/blue-1.0m.jpg" + blue_1_0m_45deg: "https://heyeuuu19.com/auto_aim/pnp/blue-1.0m-45degree.jpg" + blue_2_0m_0deg: "https://heyeuuu19.com/auto_aim/pnp/blue-2.0m.jpg" + blue_2_0m_45deg: "https://heyeuuu19.com/auto_aim/pnp/blue-2.0m-45degree.jpg" + blue_3_0m_0deg: "https://heyeuuu19.com/auto_aim/pnp/blue-3.0m.jpg" + blue_3_0m_45deg: "https://heyeuuu19.com/auto_aim/pnp/blue-3.0m-45degree.jpg" diff --git a/test/download_assets.sh b/test/download_assets.sh new file mode 100755 index 00000000..ab12af2a --- /dev/null +++ b/test/download_assets.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# 设置:如果任何命令失败,立即退出,对管道也生效 +set -eo pipefail + +# --- 配置 --- +# 获取脚本自身的目录,用于定位配置文件 +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +CONFIG_YML="$SCRIPT_DIR/asset.yml" +# 资源存放目录,优先使用 TEST_ASSETS_ROOT,默认为 /tmp/auto_aim(与测试代码一致) +DOWNLOAD_DIR="${TEST_ASSETS_ROOT:-/tmp/auto_aim}" +# 初始化为空字符串,表示当前没有文件在下载** +CURRENT_DOWNLOAD_PATH="" +# ----------------- + +# --- 信号处理函数 --- +cleanup() { + echo "" + echo "[INTERRUPT] 捕获到 Ctrl+C。开始清理..." >&2 + + if [ -n "$CURRENT_DOWNLOAD_PATH" ]; then + if [ -f "$CURRENT_DOWNLOAD_PATH" ]; then + echo "[CLEANUP] 删除不完整的文件: $CURRENT_DOWNLOAD_PATH" >&2 + rm -f "$CURRENT_DOWNLOAD_PATH" + fi + fi + echo "[EXIT] 脚本已终止。" >&2 + exit 1 +} + +trap cleanup INT TERM + +check_dependencies() { + if ! command -v yq &> /dev/null || ! command -v curl &> /dev/null; then + echo "[FATAL] 错误: 缺少必要的依赖工具 (yq 或 curl)。" >&2 + echo "请安装 yq (用于解析 YAML) 和 curl (用于下载)。" >&2 + exit 1 + fi +} + +download_asset() { + local asset_id="$1" + local url="$2" + local filename + filename=$(basename "$url") + local local_path="$DOWNLOAD_DIR/$filename" + + if [ -f "$local_path" ]; then + echo " [SKIP] $asset_id 已存在。" + return 0 + fi + + echo " [INFO] 正在下载 $asset_id -> $filename..." + + CURRENT_DOWNLOAD_PATH="$local_path" + + if curl -fsSL -o "$local_path" "$url" &> /dev/null; then + echo " [SUCCESS] -> $local_path" + CURRENT_DOWNLOAD_PATH="" + return 0 + else + echo " [ERROR] 下载失败: $asset_id (URL: $url)" >&2 + rm -f "$local_path" + CURRENT_DOWNLOAD_PATH="" + return 1 + fi +} + +main() { + local RESOURCE_MAPPING + local FAILED_COUNT=0 + + check_dependencies + + echo "--- 资源下载脚本启动 ---" + + if [ ! -f "$CONFIG_YML" ]; then + echo "[FATAL] 配置文件未找到于 $CONFIG_YML" >&2 + trap - INT TERM + exit 1 + fi + + mkdir -p "$DOWNLOAD_DIR" + + RESOURCE_MAPPING=$(yq '.resources | to_entries | .[] | "\(.key)|\(.value)"' "$CONFIG_YML") + + if [ -z "$RESOURCE_MAPPING" ]; then + echo "[WARNING] YAML 文件中未定义任何 'resources',退出。" + trap - INT TERM + exit 0 + fi + + echo "找到 $(echo "$RESOURCE_MAPPING" | wc -l | tr -d '[:space:]') 个资源需要处理。" + + while IFS='|' read -r asset_id url; do + asset_id=$(echo "$asset_id" | tr -d '"') + url=$(echo "$url" | tr -d '"') + + download_asset "$asset_id" "$url" || FAILED_COUNT=$((FAILED_COUNT + 1)) + + done <<< "$RESOURCE_MAPPING" + + echo "" + echo "--- 资源下载完成 ---" + + trap - INT TERM + + if [ "$FAILED_COUNT" -eq 0 ]; then + echo "[INFO] 所有资源下载成功或已跳过。" + exit 0 + else + echo "[ERROR] 警告:有 $FAILED_COUNT 个资源下载失败。" >&2 + exit 1 + fi +} + +main diff --git a/test/model_infer.cpp b/test/model_infer.cpp index cca80c9b..11a53927 100644 --- a/test/model_infer.cpp +++ b/test/model_infer.cpp @@ -29,6 +29,21 @@ constexpr auto config = R"( nms_threshold: 0.3 )"; +// --- 资源路径 --- +static std::filesystem::path asset_root() { + if (const char* env = std::getenv("TEST_ASSETS_ROOT"); env && *env) { + return std::filesystem::path { env }; + } + + const char* default_path = "/tmp/auto_aim"; + + return std::filesystem::path { default_path }; +} + +static std::filesystem::path asset_path(std::string_view filename) { + return asset_root() / filename; +} + TEST(model, sync_infer) { using namespace rmcs::identifier; @@ -41,13 +56,12 @@ TEST(model, sync_infer) { auto result = net.configure(yaml); ASSERT_TRUE(result.has_value()) << error_head << result.error(); - auto image_location = std::getenv("IMAGE"); - ASSERT_NE(image_location, nullptr) << error_head << "Set env 'IMAGE' to pass infer source"; + const auto image_location = asset_path("model_infer_example.jpg"); auto image { Image {} }; image.details().mat = cv::imread(image_location); ASSERT_FALSE(image.details().mat.empty()) - << error_head << std::format("Failed to read image from '{}'", image_location); + << error_head << "Failed to read image from " + std::string(image_location); const auto use_roi_segment = yaml["use_roi_segment"].as(); const auto roi_cols = yaml["roi_cols"].as(); diff --git a/test/solve_pnp.cpp b/test/solve_pnp.cpp index d62da49b..28a99a37 100644 --- a/test/solve_pnp.cpp +++ b/test/solve_pnp.cpp @@ -1,424 +1,291 @@ -#include "utility/math/solve_pnp.hpp" -#include "utility/math/linear.hpp" -#include "utility/math/point.hpp" +#include // for std::clamp, std::replace +#include // for std::getenv +#include +#include +#include // for std::setprecision +#include // for structured output +#include +#include +#include -#include -#include #include -#include +#include +#include + +#include "module/identifier/model.hpp" +#include "utility/image/image.details.hpp" +#include "utility/math/point.hpp" +#include "utility/math/solve_pnp/pnp_solution.hpp" +#include "utility/robot/armor.hpp" -using namespace rmcs; using namespace rmcs::util; +using namespace rmcs; +using Eigen::Quaterniond; using Eigen::Vector2d; using Eigen::Vector3d; -// 辅助函数:创建测试用的相机内参 -PnpSolution::Input create_test_input(double focal_length = 800.0, double cx = 320.0, - double cy = 240.0, const std::array& distort = { 0.0, 0.0, 0.0, 0.0, 0.0 }) { - PnpSolution::Input input; - input.camera_matrix = { { - { focal_length, 0.0, cx }, - { 0.0, focal_length, cy }, - { 0.0, 0.0, 1.0 }, - } }; - input.distort_coeff = distort; - return input; -} - -// 辅助函数:创建小装甲板的 3D 点 -std::array create_small_armor_shape() { - constexpr double ARMOR_WIDTH = 0.135; // 135mm - constexpr double ARMOR_HEIGHT = 0.056; // 56mm - - const auto eigen_points = std::array { - Point3d { 0.0, ARMOR_WIDTH / 2.0, ARMOR_HEIGHT / 2.0 }, // 右上 - Point3d { 0.0, -ARMOR_WIDTH / 2.0, ARMOR_HEIGHT / 2.0 }, // 右下 - Point3d { 0.0, -ARMOR_WIDTH / 2.0, -ARMOR_HEIGHT / 2.0 }, // 左下 - Point3d { 0.0, ARMOR_WIDTH / 2.0, -ARMOR_HEIGHT / 2.0 } // 左上 - }; - - auto points = std::array(); - for (size_t i = 0; i < 4; i++) { - points[i] = Point3d(eigen_points[i]); +// --- 资源路径 --- +// 测试资源路径配置: +// - 优先使用环境变量 TEST_ASSETS_ROOT +// - 未设置时默认使用 /tmp/auto_aim +// - 运行前需执行: cd test && ./download_assets.sh +static std::filesystem::path asset_root() { + if (const char* env = std::getenv("TEST_ASSETS_ROOT"); env && *env) { + return std::filesystem::path { env }; } - return points; -} - -// 辅助函数:创建大装甲板的 3D 点 -std::array create_big_armor_shape() { - constexpr double BIG_ARMOR_WIDTH = 0.230; // 230mm - constexpr double ARMOR_HEIGHT = 0.056; // 56mm - - const auto eigen_points = std::array { - Point3d { 0.0, BIG_ARMOR_WIDTH / 2.0, ARMOR_HEIGHT / 2.0 }, - Point3d { 0.0, -BIG_ARMOR_WIDTH / 2.0, ARMOR_HEIGHT / 2.0 }, - Point3d { 0.0, -BIG_ARMOR_WIDTH / 2.0, -ARMOR_HEIGHT / 2.0 }, - Point3d { 0.0, BIG_ARMOR_WIDTH / 2.0, -ARMOR_HEIGHT / 2.0 }, - }; - auto points = std::array(); - for (size_t i = 0; i < 4; i++) { - points[i] = Point3d(eigen_points[i]); - } - return points; -} + const char* default_path = "/tmp/auto_aim"; -// 辅助函数:从 Eigen::Vector2d 创建 Point2d 数组 -std::array create_armor_detection(const std::array& eigen_points) { - auto points = std::array(); - for (size_t i = 0; i < 4; i++) { - points[i] = Point2d(eigen_points[i]); - } - return points; + return std::filesystem::path { default_path }; } -// 辅助函数:验证四元数是否归一化 -bool is_quaternion_normalized(const Orientation& q, double tolerance = 0.01) { - const auto norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); - return std::abs(norm - 1.0) < tolerance; +static std::filesystem::path asset_path(std::string_view filename) { + return asset_root() / filename; } -// 辅助函数:计算两点之间的距离 -double distance(const Translation& a, const Translation& b) { - const auto dx = a.x - b.x; - const auto dy = a.y - b.y; - const auto dz = a.z - b.z; - return std::sqrt(dx * dx + dy * dy + dz * dz); -} - -// ========== 测试用例 ========== - -class PnpSolverTest : public ::testing::Test { -protected: - void SetUp() override { - // 每个测试前的设置 - } - - void TearDown() override { - // 每个测试后的清理 - } +// --- 测试数据 --- +struct PnpTestCase { + std::string filename; + double expected_distance_m; // 预期的目标距离(米) + double expected_angle_deg; // 预期的偏航角(度,0 或 45) }; -TEST_F(PnpSolverTest, BasicSmallArmor) { - auto solution = PnpSolution {}; - - solution.input = create_test_input(); - solution.input.armor_shape = create_small_armor_shape(); - - // 使用 Eigen::Vector2d 创建 2D 检测点 - const auto eigen_detection = std::array { - Vector2d { 350.0, 220.0 }, - Vector2d { 290.0, 220.0 }, - Vector2d { 290.0, 260.0 }, - Vector2d { 350.0, 260.0 }, - }; - - solution.input.armor_detection = create_armor_detection(eigen_detection); - - // 执行求解 - EXPECT_NO_THROW(solution.solve()); - - // 验证结果 - EXPECT_GT(solution.result.translation.z, 0.0) // - << "Translation z should be positive (in front of camera)"; - - EXPECT_TRUE(is_quaternion_normalized(solution.result.orientation)) // - << "Quaternion should be normalized"; - - const auto trans_norm = std::sqrt(solution.result.translation.x * solution.result.translation.x - + solution.result.translation.y * solution.result.translation.y - + solution.result.translation.z * solution.result.translation.z); - - EXPECT_GT(trans_norm, 0.0) << "Translation should not be zero"; -} - -TEST_F(PnpSolverTest, BigArmor) { - auto solution = PnpSolution {}; - - solution.input = create_test_input(); - solution.input.armor_shape = create_big_armor_shape(); - - // 使用 Eigen::Vector2d 创建 2D 检测点 - const auto eigen_detection = std::array { - Vector2d { 380.0, 200.0 }, - Vector2d { 260.0, 200.0 }, - Vector2d { 260.0, 280.0 }, - Vector2d { 380.0, 280.0 }, - }; - - solution.input.armor_detection = create_armor_detection(eigen_detection); +// 所有本地测试数据 +const std::vector kPnpTestCases = { + { "blue-0.5m.jpg", 0.5, 0.0 }, + { "blue-0.5m-45degree.jpg", 0.5, 45.0 }, + { "blue-1.0m.jpg", 1.0, 0.0 }, + { "blue-1.0m-45degree.jpg", 1.0, 45.0 }, + { "blue-2.0m.jpg", 2.0, 0.0 }, + { "blue-2.0m-45degree.jpg", 2.0, 45.0 }, + { "blue-3.0m.jpg", 3.0, 0.0 }, + { "blue-3.0m-45degree.jpg", 3.0, 45.0 }, +}; - // 执行求解 - EXPECT_NO_THROW(solution.solve()); +// --- 资源读取辅助函数 --- +// --- 核心辅助函数:相机/装甲板参数 --- +// 辅助函数:创建测试用的相机内参 +PnpSolution::Input create_test_input(double fx = 1.722231837421459e+03, + double fy = 1.724876404292754e+03, double cx = 7.013056440882832e+02, + double cy = 5.645821718351237e+02, double k1 = -0.064232403853946, + double k2 = -0.087667493884102, double k3 = 0.792381808294582) { - // 验证结果 - EXPECT_GT(solution.result.translation.z, 0.0) // - << "Translation z should be positive (in front of camera)"; + auto distort_coeff = std::array { k1, k2, 0, 0, k3 }; + PnpSolution::Input input {}; + input.camera_matrix = { { + { fx, 0.0, cx }, + { 0.0, fy, cy }, + { 0.0, 0.0, 1.0 }, + } }; + input.distort_coeff = distort_coeff; - EXPECT_TRUE(is_quaternion_normalized(solution.result.orientation)) // - << "Quaternion should be normalized"; + return input; } -TEST_F(PnpSolverTest, DifferentDistances) { - constexpr double FOCAL_LENGTH = 800.0; - constexpr double ARMOR_WIDTH = 0.135; - - const auto test_distances = std::vector { 1.0, 2.0, 3.0, 5.0 }; - - for (const auto expected_distance : test_distances) { - auto solution = PnpSolution {}; - - solution.input = create_test_input(FOCAL_LENGTH); - solution.input.armor_shape = create_small_armor_shape(); - - // 根据距离计算图像中的大小 - const auto pixel_size = (FOCAL_LENGTH * ARMOR_WIDTH) / expected_distance; - const auto center = Vector2d { 320.0, 240.0 }; - - const auto eigen_detection = std::array { - center + Vector2d { pixel_size / 2, -pixel_size * 0.2 }, - center + Vector2d { -pixel_size / 2, -pixel_size * 0.2 }, - center + Vector2d { -pixel_size / 2, pixel_size * 0.2 }, - center + Vector2d { pixel_size / 2, pixel_size * 0.2 }, - }; - - solution.input.armor_detection = create_armor_detection(eigen_detection); - - // 执行求解 - EXPECT_NO_THROW(solution.solve()); - - // 验证结果 - const auto distance_error = std::abs(solution.result.translation.z - expected_distance); - - EXPECT_LT(distance_error, expected_distance * 0.5) // - << "Distance error too large for expected distance " << expected_distance; - - EXPECT_TRUE(is_quaternion_normalized(solution.result.orientation)) // - << "Quaternion should be normalized"; +// 辅助函数:使用 OpenCV 坐标系定义的装甲板 3D 点 +// [Top Left, Top Right, Bottom Right, Bottom Left] +std::array create_small_armor_shape() { return rmcs::kSmallArmorShapeOpenCV; } + +std::array infer_armor_detection_from_file(std::string_view filename) { + using namespace rmcs::identifier; + + constexpr auto config_yaml = R"( + model_location: "assets/yolov5.xml" + infer_device: "AUTO" + use_roi_segment: false + use_corner_correction: false + roi_rows: 640 + roi_cols: 640 + input_rows: 640 + input_cols: 640 + min_confidence: 0.8 + score_threshold: 0.7 + nms_threshold: 0.3 + )"; + + auto net = OpenVinoNet {}; + auto yaml = YAML::Load(config_yaml); + + const auto location = std::filesystem::path { __FILE__ }.parent_path(); + auto model_location = location / "../models/yolov5.xml"; + yaml["model_location"] = model_location.string(); + + auto cfg_result = net.configure(yaml); + if (!cfg_result.has_value()) { + throw std::runtime_error("Failed to configure OpenVinoNet: " + cfg_result.error()); } -} - -TEST_F(PnpSolverTest, EdgeCaseSmallDetection) { - auto solution = PnpSolution {}; - - solution.input = create_test_input(); - solution.input.armor_shape = create_small_armor_shape(); - - // 使用 Eigen::Vector2d 创建 2D 检测点(小检测框,远距离) - const auto eigen_detection = std::array { - Vector2d { 322.0, 238.0 }, - Vector2d { 318.0, 238.0 }, - Vector2d { 318.0, 242.0 }, - Vector2d { 322.0, 242.0 }, - }; - solution.input.armor_detection = create_armor_detection(eigen_detection); - - // 执行求解 - EXPECT_NO_THROW(solution.solve()); - - // 验证结果 - EXPECT_GT(solution.result.translation.z, 0.0) // - << "Translation z should be positive (in front of camera)"; - - EXPECT_TRUE(is_quaternion_normalized(solution.result.orientation)) // - << "Quaternion should be normalized"; -} + const auto full_path = asset_path(filename); + auto cv_mat = cv::imread(full_path.string(), cv::IMREAD_COLOR); + if (cv_mat.empty()) { + throw std::runtime_error("Failed to read image: " + full_path.string()); + } -TEST_F(PnpSolverTest, WithDistortion) { - auto solution = PnpSolution {}; + auto image = rmcs::Image {}; + image.details().mat = cv_mat; - solution.input = create_test_input(800.0, 320.0, 240.0, { 0.1, -0.2, 0.0, 0.0, 0.0 }); - solution.input.armor_shape = create_small_armor_shape(); + auto infer_result = net.sync_infer(image); + if (!infer_result.has_value()) { + throw std::runtime_error("OpenVino inference failed: " + infer_result.error()); + } + const auto& armors = infer_result.value(); + if (armors.empty()) { + throw std::runtime_error("No armor detected from image: " + full_path.string()); + } - // 使用 Eigen::Vector2d 创建 2D 检测点 - const auto eigen_detection = std::array { - Vector2d { 350.0, 220.0 }, - Vector2d { 290.0, 220.0 }, - Vector2d { 290.0, 260.0 }, - Vector2d { 350.0, 260.0 }, + const auto& armor = armors.front(); + // [Top Left, Top Right, Bottom Right, Bottom Left] 与 3D 坐标定义一致 + return std::array { + Point2d { armor.tl() }, // 0 + Point2d { armor.tr() }, // 1 + Point2d { armor.br() }, // 2 + Point2d { armor.bl() }, // 3 }; - - solution.input.armor_detection = create_armor_detection(eigen_detection); - - // 执行求解 - EXPECT_NO_THROW(solution.solve()); - - // 验证结果 - EXPECT_GT(solution.result.translation.z, 0.0) // - << "Translation z should be positive (in front of camera)"; - - EXPECT_TRUE(is_quaternion_normalized(solution.result.orientation)) // - << "Quaternion should be normalized"; } -TEST_F(PnpSolverTest, QuaternionValidity) { - auto solution = PnpSolution {}; - - solution.input = create_test_input(); - solution.input.armor_shape = create_small_armor_shape(); - - // 使用 Eigen::Vector2d 创建 2D 检测点 - const auto eigen_detection = std::array { - Vector2d { 350.0, 220.0 }, - Vector2d { 290.0, 220.0 }, - Vector2d { 290.0, 260.0 }, - Vector2d { 350.0, 260.0 }, - }; - - solution.input.armor_detection = create_armor_detection(eigen_detection); +// --- PnP 结果处理辅助函数 --- - // 执行求解 - solution.solve(); +/** + * @brief 将弧度值规范化到 [-PI/2, PI/2] 范围内 (对应于 [-90度, 90度])。 + * @return 规范化后的角度,范围在 [-PI/2, PI/2] 之间 (弧度)。 + */ +double normalize_angle_90(double angle_rad) { + constexpr double PI = M_PI; + constexpr double HALF_PI = PI / 2.0; - // 验证四元数的有效性 - const auto& q = solution.result.orientation; - - EXPECT_TRUE(std::isfinite(q.x)) << "Quaternion x should be finite"; - EXPECT_TRUE(std::isfinite(q.y)) << "Quaternion y should be finite"; - EXPECT_TRUE(std::isfinite(q.z)) << "Quaternion z should be finite"; - EXPECT_TRUE(std::isfinite(q.w)) << "Quaternion w should be finite"; + // 1. 将角度限制在 [-PI, PI] 范围内 + double normalized = std::fmod(angle_rad + PI, 2.0 * PI); + if (normalized < 0) { + normalized += 2.0 * PI; + } + normalized -= PI; - const auto norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); + // 2. 将角度从 [-PI, PI] 映射到 [-PI/2, PI/2] (180度对称处理) + if (normalized > HALF_PI) { + normalized = PI - normalized; + } else if (normalized < -HALF_PI) { + normalized = -PI - normalized; + } - EXPECT_NEAR(norm, 1.0, 0.01) << "Quaternion should be normalized"; + return std::clamp(normalized, -HALF_PI, HALF_PI); } -TEST_F(PnpSolverTest, TranslationValidity) { - auto solution = PnpSolution {}; - - solution.input = create_test_input(); - solution.input.armor_shape = create_small_armor_shape(); - - // 使用 Eigen::Vector2d 创建 2D 检测点 - const auto eigen_detection = std::array { - Vector2d { 350.0, 220.0 }, - Vector2d { 290.0, 220.0 }, - Vector2d { 290.0, 260.0 }, - Vector2d { 350.0, 260.0 }, - }; - - solution.input.armor_detection = create_armor_detection(eigen_detection); - - // 执行求解 - solution.solve(); - - // 验证平移向量的有效性 - const auto& t = solution.result.translation; - - EXPECT_TRUE(std::isfinite(t.x)) << "Translation x should be finite"; - EXPECT_TRUE(std::isfinite(t.y)) << "Translation y should be finite"; - EXPECT_TRUE(std::isfinite(t.z)) << "Translation z should be finite"; - - EXPECT_GT(t.z, 0.0) << "Translation z should be positive"; +// 四元数转 ZYX 欧拉角 (Yaw, Pitch, Roll),单位:弧度 +static Eigen::Vector3d quaternion_to_euler_rad(const Orientation& q) { + Quaterniond quat(q.w, q.x, q.y, q.z); // Eigen 构造函数期望 (w,x,y,z) + const auto euler = quat.toRotationMatrix().eulerAngles(2, 1, 0); // yaw(Z), pitch(Y), roll(X) + return { euler[0], euler[1], euler[2] }; } -TEST_F(PnpSolverTest, DifferentFocalLengths) { - const auto focal_lengths = std::vector { 400.0, 800.0, 1200.0, 1600.0 }; - - for (const auto focal : focal_lengths) { - auto solution = PnpSolution {}; +// --- 参数化测试类 --- +class PnpSolverParameterizedTest : public ::testing::TestWithParam { +protected: + PnpSolution solution; + PnpTestCase test_case; + double actual_distance; + double folded_yaw_deg; + double distance_error; + double yaw_error; + const double max_allowed_distance_error_ratio = 0.08; // 8% + const double max_allowed_yaw_error_deg = 15.0; // 15 degrees - solution.input = create_test_input(focal); + void SetUp() override { + test_case = GetParam(); + solution.input = create_test_input(); solution.input.armor_shape = create_small_armor_shape(); - const auto scale = focal / 800.0; - const auto center = Vector2d { 320.0, 240.0 }; - const auto offset = Vector2d { 30.0, 20.0 }; - - const auto eigen_detection = std::array { - center + Vector2d { offset.x() * scale, -offset.y() * scale }, - center + Vector2d { -offset.x() * scale, -offset.y() * scale }, - center + Vector2d { -offset.x() * scale, offset.y() * scale }, - center + Vector2d { offset.x() * scale, offset.y() * scale }, - }; - - solution.input.armor_detection = create_armor_detection(eigen_detection); - - // 执行求解 - EXPECT_NO_THROW(solution.solve()); - - // 验证结果 - EXPECT_GT(solution.result.translation.z, 0.0) // - << "Translation z should be positive (in front of camera)"; - - EXPECT_TRUE(is_quaternion_normalized(solution.result.orientation)) // - << "Quaternion should be normalized"; - } -} - -TEST_F(PnpSolverTest, Consistency) { - auto solution1 = PnpSolution {}; - auto solution2 = PnpSolution {}; + const auto detection = infer_armor_detection_from_file(test_case.filename); + solution.input.armor_detection = detection; - solution1.input = create_test_input(); - solution1.input.armor_shape = create_small_armor_shape(); + std::cerr << std::fixed << std::setprecision(1); + std::cerr << "[2D_POINTS] | FILE: " << test_case.filename << " | TL(" << detection[0].x + << "," << detection[0].y << ")" + << " TR(" << detection[1].x << "," << detection[1].y << ")" + << " BR(" << detection[2].x << "," << detection[2].y << ")" + << " BL(" << detection[3].x << "," << detection[3].y << ")" << std::endl; + std::cerr << std::defaultfloat; // 恢复默认浮点格式 - // 使用 Eigen::Vector2d 创建 2D 检测点 - const auto eigen_detection = std::array { - Vector2d { 350.0, 220.0 }, - Vector2d { 290.0, 220.0 }, - Vector2d { 290.0, 260.0 }, - Vector2d { 350.0, 260.0 }, - }; - - solution1.input.armor_detection = create_armor_detection(eigen_detection); - solution2.input = solution1.input; // 相同输入 - - // 执行求解 - solution1.solve(); - solution2.solve(); - - // 验证一致性 - const auto trans_diff = distance(solution1.result.translation, solution2.result.translation); + SCOPED_TRACE(test_case.filename); - EXPECT_LT(trans_diff, 0.001) << "Results should be consistent"; + EXPECT_NO_THROW(solution.solve()) + << "Pnp solve failed for file: " << asset_path(test_case.filename); - const auto quat_diff = - std::sqrt(std::pow(solution1.result.orientation.x - solution2.result.orientation.x, 2) - + std::pow(solution1.result.orientation.y - solution2.result.orientation.y, 2) - + std::pow(solution1.result.orientation.z - solution2.result.orientation.z, 2) - + std::pow(solution1.result.orientation.w - solution2.result.orientation.w, 2)); + // --- 解算结果处理 --- + const auto& result = solution.result; + actual_distance = result.translation.x; + distance_error = std::abs(actual_distance - test_case.expected_distance_m); - EXPECT_TRUE(quat_diff < 0.001 || quat_diff > 1.9) // - << "Quaternions should be consistent"; -} - -TEST_F(PnpSolverTest, Performance) { - auto solution = PnpSolution {}; + const auto euler_rad = quaternion_to_euler_rad(result.orientation); + const double actual_yaw_rad = euler_rad[0]; - solution.input = create_test_input(); - solution.input.armor_shape = create_small_armor_shape(); - - // 使用 Eigen::Vector2d 创建 2D 检测点 - const auto eigen_detection = std::array { - Vector2d { 350.0, 220.0 }, - Vector2d { 290.0, 220.0 }, - Vector2d { 290.0, 260.0 }, - Vector2d { 350.0, 260.0 }, - }; - - solution.input.armor_detection = create_armor_detection(eigen_detection); - - constexpr int iterations = 1000; - const auto start = std::chrono::high_resolution_clock::now(); + const double folded_yaw_rad = normalize_angle_90(actual_yaw_rad); + folded_yaw_deg = folded_yaw_rad * 180.0 / M_PI; + yaw_error = std::abs(folded_yaw_deg - test_case.expected_angle_deg); + } - for (int i = 0; i < iterations; i++) { - solution.solve(); + // 打印结构化报告 + void PrintStructuredReport() const { + std::cerr << std::fixed << std::setprecision(4); + std::cerr << "[TEST_REPORT] |" << std::left << std::setw(50) << test_case.filename << "|"; + + // 距离信息 + std::cerr << " DISTANCE: " << std::setw(6) << actual_distance << "m (Exp: " << std::setw(4) + << test_case.expected_distance_m << "m) | Error: " << std::setw(6) + << distance_error << "m | Status: "; + if (distance_error < test_case.expected_distance_m * max_allowed_distance_error_ratio) { + std::cerr << "PASS |"; + } else { + std::cerr << "FAIL |"; + } + + // 角度信息 + std::cerr << " YAW: " << std::setw(6) << folded_yaw_deg << "deg (Exp: " << std::setw(4) + << test_case.expected_angle_deg << "deg) | Error: " << std::setw(6) << yaw_error + << "deg | Status: "; + if (yaw_error < max_allowed_yaw_error_deg) { + std::cerr << "PASS |" << std::endl; + } else { + std::cerr << "FAIL |" << std::endl; + } } +}; - const auto end = std::chrono::high_resolution_clock::now(); - const auto duration = std::chrono::duration_cast(end - start); +// 核心测试:距离和角度精度 +TEST_P(PnpSolverParameterizedTest, DistanceAndAngleAccuracy) { + this->PrintStructuredReport(); - const auto avg_time_us = static_cast(duration.count()) / iterations; + // 1. 距离断言 (8% 误差) + const double max_dist_error = + GetParam().expected_distance_m * this->max_allowed_distance_error_ratio; - std::cout << "Average solve time: " << avg_time_us << " microseconds\n"; + EXPECT_LT(this->distance_error, max_dist_error) + << "Distance error (" << this->distance_error << "m) exceeded " << max_dist_error + << "m for " << GetParam().filename; - EXPECT_LT(avg_time_us, 1000.0) << "Solve should be fast enough"; + // 2. 角度断言 (15度误差) + EXPECT_LT(this->yaw_error, this->max_allowed_yaw_error_deg) + << "Yaw error (" << this->yaw_error << "deg) exceeded " << this->max_allowed_yaw_error_deg + << "deg for " << GetParam().filename; } +// 注册参数化测试 +INSTANTIATE_TEST_SUITE_P(AllImageTests, PnpSolverParameterizedTest, + ::testing::ValuesIn(kPnpTestCases), [](const ::testing::TestParamInfo& info) { + std::string name = info.param.filename; + std::replace(name.begin(), name.end(), '.', '_'); + std::replace(name.begin(), name.end(), '-', '_'); + return name; + }); + // 主函数 int main(int argc, char** argv) { + std::cout << "\n--- Starting PnP Accuracy Tests ---\n"; + std::cout << "[TEST_REPORT] | URL (Simplified Name) | DISTANCE: " + "Actual (Exp) | Error | Status | YAW: Actual (Exp) | Error | Status |\n"; + ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + int result = RUN_ALL_TESTS(); + + std::cout << "--- PnP Accuracy Tests Finished ---\n"; + return result; } diff --git a/test/static_tf.cpp b/test/static_tf.cpp index d95284e1..c2223731 100644 --- a/test/static_tf.cpp +++ b/test/static_tf.cpp @@ -32,6 +32,10 @@ TEST(static_tf, construct) { SentryTf::foreach_df_with_parent( [](auto parent) { std::println("{} -> {}", parent, T::name); }); + using Result = SentryTf::Find<"0.0.0">::Result; + + constexpr auto result = SentryTf::find<"0.0.0">(); + static_assert(SentryTf::name == "0"); static_assert(SentryTf::child_amount > 0); static_assert(SentryTf::total_amount > 0); diff --git a/test/transform_communication.cpp b/test/transform_communication.cpp new file mode 100644 index 00000000..6e8f68cb --- /dev/null +++ b/test/transform_communication.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include + +#include "utility/shared/context.hpp" +#include "utility/shared/interprocess.hpp" +#include + +using Transform = rmcs::util::Transform; + +namespace { + +constexpr auto create_input_transform() -> Transform { + using rmcs::Direction3d; + using rmcs::Orientation; + return { + .posture = Direction3d { 1.0, 2.0, 3.0 }, + .orientation = Orientation { 0.0, 0.0, 0.0, 1.0 }, + }; +} + +// 非期望值,用于验证接收后被正确覆盖 +inline auto create_failed_input_transform() -> Transform { + using rmcs::Direction3d; + using rmcs::Orientation; + return { + .posture = Direction3d { 2.0, 3.0, 3.0 }, + .orientation = Orientation { Eigen::Quaterniond::Identity() }, + }; +} + +inline auto expect_transform_equal(const Transform& lhs, const Transform& rhs) -> void { + EXPECT_DOUBLE_EQ(lhs.posture.x, rhs.posture.x); + EXPECT_DOUBLE_EQ(lhs.posture.y, rhs.posture.y); + EXPECT_DOUBLE_EQ(lhs.posture.z, rhs.posture.z); + EXPECT_DOUBLE_EQ(lhs.orientation.x, rhs.orientation.x); + EXPECT_DOUBLE_EQ(lhs.orientation.y, rhs.orientation.y); + EXPECT_DOUBLE_EQ(lhs.orientation.z, rhs.orientation.z); + EXPECT_DOUBLE_EQ(lhs.orientation.w, rhs.orientation.w); +} + +} // namespace + +// 父子进程通信:确保写入的 Transform 能被完整读取 +TEST(TransformShm, SendRecvSequence) { + using Send = rmcs::shm::Client::Send; + using Recv = rmcs::shm::Client::Recv; + + using namespace std::chrono_literals; + + constexpr auto shm_name = "/rmcs_auto_aim_transform_shm_test"; + constexpr auto test_value = create_input_transform(); + constexpr auto max_attempts = 100; + constexpr auto poll_interval = 10ms; + constexpr auto init_delay = 50ms; + + // 父进程先创建共享内存对象 + auto send = Send {}; + ASSERT_TRUE(send.open(shm_name)); + ASSERT_TRUE(send.opened()); + + auto pid = fork(); + ASSERT_GE(pid, 0); + + if (pid == 0) { + // 子进程:接收数据 + auto recv = Recv {}; + ASSERT_TRUE(recv.open(shm_name)); + ASSERT_TRUE(recv.opened()); + + // 等待接收数据 + auto received_value = create_failed_input_transform(); + auto received = false; + + // 尝试接收数据,最多等待一段时间 + for (auto i = 0; i < max_attempts; ++i) { + if (recv.is_updated()) { + recv.with_read([&](const auto& data) { received_value = data; }); + received = true; + break; + } + std::this_thread::sleep_for(poll_interval); + } + + ASSERT_TRUE(received && "failed to receive"); + + expect_transform_equal(received_value, test_value); + + std::exit(0); + } else { + // 父进程:发送数据 + // 等待一下子进程准备好 + std::this_thread::sleep_for(init_delay); + + // 发送数据 + send.with_write([&](auto& data) { data = test_value; }); + + // 等待子进程完成 + auto status = int { 0 }; + waitpid(pid, &status, 0); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), 0); + } +} + +// 单进程读写:验证版本控制与快照读取的正确性,模拟 component 中动态变换的发布/消费 +TEST(TransformShm, SnapshotAndUpdate) { + using Client = rmcs::shm::Client; + Client::Send send; + Client::Recv recv; + + constexpr auto shm_name = "/rmcs_auto_aim_snapshot_test"; + + ASSERT_TRUE(send.open(shm_name)); + ASSERT_TRUE(recv.open(shm_name)); + + const auto first = create_input_transform(); + const auto second = create_failed_input_transform(); + + // 初次写入,接收端应检测到更新 + send.send(first); + EXPECT_TRUE(recv.is_updated()); + + Transform snapshot {}; + recv.with_read([&](const auto& data) { snapshot = data; }); + expect_transform_equal(snapshot, first); + EXPECT_FALSE(recv.is_updated()); // 读取后版本同步 + + // 再写入一次不同数据,接收端应再次检测到更新 + send.with_write([&](auto& data) { data = second; }); + EXPECT_TRUE(recv.is_updated()); + + recv.recv(snapshot); // 使用 recv 接口读取一次 + expect_transform_equal(snapshot, second); + EXPECT_FALSE(recv.is_updated()); +} diff --git a/tool/CMakeLists.txt b/tool/CMakeLists.txt index 37e5875a..e54354a7 100644 --- a/tool/CMakeLists.txt +++ b/tool/CMakeLists.txt @@ -11,7 +11,6 @@ find_package(rclcpp REQUIRED) find_package(visualization_msgs REQUIRED) find_package(geometry_msgs REQUIRED) -find_package(yaml-cpp REQUIRED) find_package(OpenCV 4 REQUIRED) include_directories( diff --git a/tool/visualization.cpp b/tool/visualization.cpp index df746d9c..2b0a7bc0 100644 --- a/tool/visualization.cpp +++ b/tool/visualization.cpp @@ -35,15 +35,15 @@ auto main() -> int { .rclcpp = visual, .device = DeviceId::SENTRY, .camp = CampColor::BLUE, - .id = "", + .id = 0, + .name = "visual_test_armor", .tf = "camera_link", }; - auto& name = config.id; - auto index = char { 'a' }; - std::ranges::for_each(armors, [&](auto& armor) { - name = std::string { "sentry/" } + index++; - armor = std::make_unique(config); - }); + + for (auto i = 0; i < (int)armors.size(); ++i) { + config.id = i; + armors[i] = std::make_unique(config); + } } auto posture = std::make_unique( //