diff --git a/CMakeLists.txt b/CMakeLists.txt
index fecb845..cafb540 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -31,6 +31,7 @@ find_package(ament_index_cpp REQUIRED)
find_package(rclcpp REQUIRED)
find_package(visualization_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
+find_package(std_msgs REQUIRED)
find_package(tf2_ros REQUIRED)
find_package(pluginlib REQUIRED)
@@ -71,6 +72,7 @@ target_include_directories(
PRIVATE
${visualization_msgs_INCLUDE_DIRS}
${geometry_msgs_INCLUDE_DIRS}
+ ${std_msgs_INCLUDE_DIRS}
${tf2_ros_INCLUDE_DIRS}
)
target_link_libraries(
@@ -84,6 +86,7 @@ target_link_libraries(
PRIVATE
${visualization_msgs_LIBRARIES}
${geometry_msgs_LIBRARIES}
+ ${std_msgs_LIBRARIES}
${tf2_ros_LIBRARIES}
)
diff --git a/config/config.yaml b/config/config.yaml
index 9c01e30..5c437f3 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -20,8 +20,7 @@ capturer:
reconnect_wait_interval: 100
record_enable: true
- saving_pathes:
- ["/home/root/autoaim/", "/home/ubuntu/autoaim/", "/tmp/autoaim/"]
+ saving_pathes: ["/root/autoaim/", "/home/ubuntu/autoaim/", "/tmp/autoaim/"]
max_duration_seconds: 60
record_fps: 24
max_videos_size_gb: 50
diff --git a/package.xml b/package.xml
index c986fdd..ef36406 100644
--- a/package.xml
+++ b/package.xml
@@ -17,6 +17,7 @@
rmcs_executor
rmcs_description
fast_tf
+ std_msgs
rmcs_msgs
hikcamera
diff --git a/src/kernel/auto_aim.cpp b/src/kernel/auto_aim.cpp
index 7e8f906..fee5d75 100644
--- a/src/kernel/auto_aim.cpp
+++ b/src/kernel/auto_aim.cpp
@@ -50,6 +50,8 @@ struct AutoAim::Impl {
auto image = cap.fetch_image();
if (!image) continue;
+ // @TODO:
+ // 避免拷贝,而是把需要的具体量拿出来
auto context = SystemContext::kIdentity();
{
std::lock_guard lock { self.context_mutex };
@@ -63,9 +65,9 @@ struct AutoAim::Impl {
[[maybe_unused]] auto streamer = std::experimental::scope_exit { [&] {
visual.draw_later( // 录制开关
- Text { cap.recording() ? "RECORD ON" : "RECORD OFF", { 10, 700 } });
+ Canvas::Text { cap.recording() ? "RECORD ON" : "RECORD OFF", { 10, 700 } });
visual.draw_later( // 自瞄帧率
- Text { std::format("FPS: {}", framerate.fps()), { 10, 680 } });
+ Canvas::Text { std::format("FPS: {}", framerate.fps()), { 10, 680 } });
visual.update_image(*image);
} };
@@ -148,8 +150,10 @@ struct AutoAim::Impl {
cmd.yaw_acc, cmd.pitch_acc);
}
}
- visual.draw_later(Text { "ATTACK", { 10, 660 }, cmd.should_shoot ? kRed : kWhite });
- visual.draw_later(Text { "CONTROL", { 10, 640 }, cmd.should_control ? kRed : kWhite });
+ visual.draw_later(
+ Canvas::Text { "ATTACK", { 10, 660 }, cmd.should_shoot ? kRed : kWhite });
+ visual.draw_later(
+ Canvas::Text { "CONTROL", { 10, 640 }, cmd.should_control ? kRed : kWhite });
{
std::lock_guard lock { self.command_mutex };
diff --git a/src/kernel/visualization.cpp b/src/kernel/visualization.cpp
index 3c48f41..9c96893 100644
--- a/src/kernel/visualization.cpp
+++ b/src/kernel/visualization.cpp
@@ -7,6 +7,7 @@
#include "utility/rclcpp/visual/armor.hpp"
#include "utility/rclcpp/visual/arrow.hpp"
#include "utility/rclcpp/visual/lightbar.hpp"
+#include "utility/rclcpp/visual/scalar.hpp"
#include "utility/rclcpp/visual/transform.hpp"
#include "utility/serializable.hpp"
@@ -61,6 +62,7 @@ struct Visualization::Impl {
std::unique_ptr aiming_direction;
std::unordered_map armor_publishers;
std::unordered_map lightbar_publishers;
+ std::unordered_map scalar_publishers;
bool is_initialized = false;
bool size_determined = false;
@@ -141,7 +143,6 @@ struct Visualization::Impl {
auto publish(std::span armors, const std::string& name) {
if (!is_initialized || !config.publishable) return;
-
auto iter = armor_publishers.find(name);
if (iter == armor_publishers.end()) {
auto config = visual::Armors::Config {
@@ -155,9 +156,7 @@ struct Visualization::Impl {
}
auto publish(std::span lightbars, const std::string& name) -> void {
- if (!is_initialized) return;
- if (!config.publishable) return;
-
+ if (!is_initialized || !config.publishable) return;
auto iter = lightbar_publishers.find(name);
if (iter == lightbar_publishers.end()) {
auto config = visual::Lightbars::Config {
@@ -175,6 +174,15 @@ struct Visualization::Impl {
odom_transform.publish(t);
}
+ auto publish(double value, const std::string& name) -> void {
+ if (!is_initialized || !config.publishable) return;
+ auto iter = scalar_publishers.find(name);
+ if (iter == scalar_publishers.end()) {
+ iter = scalar_publishers.emplace(name, visual::Scalar { rclcpp, name }).first;
+ }
+ iter->second.publish(value);
+ }
+
auto update_aiming_direction(double yaw, double pitch) const -> void {
if (!is_initialized) return;
if (!config.publishable) return;
@@ -226,6 +234,10 @@ auto Visualization::publish(const Transform& t, const std::string& name) -> void
pimpl->publish(t, name);
}
+auto Visualization::publish(double value, const std::string& name) -> void {
+ pimpl->publish(value, name);
+}
+
Visualization::Visualization() noexcept
: pimpl { std::make_unique() } { }
diff --git a/src/kernel/visualization.hpp b/src/kernel/visualization.hpp
index b4d38c6..463401c 100644
--- a/src/kernel/visualization.hpp
+++ b/src/kernel/visualization.hpp
@@ -38,6 +38,8 @@ class Visualization {
auto publish(const Transform& t, const std::string& name) -> void;
+ auto publish(double value, const std::string& name) -> void;
+
auto update_aiming_direction(double yaw, double pitch) const -> void;
auto update_mpc_plan(double yaw, double pitch, double yaw_rate, double pitch_rate,
diff --git a/src/module/perception/rotation_estimator.cpp b/src/module/perception/rotation_estimator.cpp
new file mode 100644
index 0000000..fff9b71
--- /dev/null
+++ b/src/module/perception/rotation_estimator.cpp
@@ -0,0 +1,419 @@
+/// @brief 基于成熟 VIO 视觉前端思想的轻量旋转速度估计器。
+///
+/// 算法来源:
+/// - VINS-Mono FeatureTracker:
+/// https://github.com/HKUST-Aerial-Robotics/VINS-Mono/blob/master/feature_tracker/src/feature_tracker.cpp
+/// - OpenVINS TrackKLT:
+/// https://github.com/rpng/open_vins/blob/master/ov_core/src/track/TrackKLT.cpp
+///
+/// 本实现没有复制上述 GPL 项目源码,只参考其通用流程:维护上一帧特征点,使用 KLT
+/// 光流跟踪到当前帧,通过 mask 排除装甲板等动态区域,使用 RANSAC 剔除外点,再根据
+/// 归一化图像平面上的背景点速度输出 yaw/pitch/roll-like 视觉旋转速度波形。
+
+#include "rotation_estimator.hpp"
+#include "utility/image/image.details.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+using namespace rmcs;
+
+struct RotationEstimator::Impl {
+ static constexpr auto kMaxFeatures = 160;
+ static constexpr auto kMinTrackedPoints = 6;
+ static constexpr auto kMinInlierPoints = 4;
+ static constexpr auto kRefillThreshold = 80;
+ static constexpr auto kMinFeatureDist = 18.0;
+ static constexpr auto kMaskPaddingRatio = 0.65;
+ static constexpr auto kScale = 0.3;
+ static constexpr auto kMinDt = 1e-4;
+ static constexpr auto kMaxDt = 0.2;
+ static constexpr auto kBorderSize = 8;
+ static constexpr auto kMaxOpticalFlowErr = 80.0f;
+ static constexpr auto kGridStep = 48;
+
+ struct Tracks {
+ std::vector prev;
+ std::vector current;
+ std::vector prev_inliers;
+ std::vector current_inliers;
+ double roll_rate_like { 0.0 };
+ };
+
+ cv::Mat prev_gray;
+ cv::Mat armor_mask;
+ cv::Mat lightbar_mask;
+ TimePoint prev_timestamp { };
+ std::vector prev_points;
+
+ cv::Mat camera_matrix;
+ cv::Mat distort_coeff;
+ Addition addition { };
+
+ static auto in_border(const cv::Point2f& point, const cv::Size& size) -> bool {
+ const auto border = static_cast(kBorderSize);
+ const auto width = static_cast(size.width - kBorderSize);
+ const auto height = static_cast(size.height - kBorderSize);
+
+ return point.x >= border && point.y >= border && point.x < width && point.y < height;
+ }
+
+ static auto mask_allows(const cv::Mat& mask, const cv::Point2f& point) -> bool {
+ const auto x = static_cast(std::lround(point.x));
+ const auto y = static_cast(std::lround(point.y));
+ if (x < 0 || y < 0 || x >= mask.cols || y >= mask.rows) return false;
+
+ return mask.at(y, x) != 0;
+ }
+
+ static auto median(std::vector values) -> double {
+ if (values.empty()) return 0.0;
+
+ const auto middle = values.begin() + static_cast(values.size() / 2);
+ std::nth_element(values.begin(), middle, values.end());
+
+ auto result = *middle;
+ if (values.size() % 2 == 0) {
+ const auto lower = std::max_element(values.begin(), middle);
+ result = (*lower + result) * 0.5;
+ }
+ return result;
+ }
+
+ static auto detect_features(const cv::Mat& gray, const cv::Mat& allowed_mask,
+ const std::vector& existing) -> std::vector {
+ auto feature_mask = allowed_mask.clone();
+ auto compacted = std::vector { };
+ compacted.reserve(kMaxFeatures);
+
+ for (const auto& point : existing) {
+ if (!in_border(point, gray.size()) || !mask_allows(allowed_mask, point)) continue;
+
+ compacted.push_back(point);
+ cv::circle(feature_mask, point, static_cast(kMinFeatureDist), cv::Scalar { 0 },
+ cv::FILLED);
+
+ if (static_cast(compacted.size()) >= kMaxFeatures) return compacted;
+ }
+
+ {
+ auto new_points = std::vector { };
+ const auto needed = kMaxFeatures - static_cast(compacted.size());
+ if (needed > 0) {
+ cv::goodFeaturesToTrack(
+ gray, new_points, needed, 0.01, kMinFeatureDist, feature_mask, 3, false, 0.04);
+ }
+ compacted.insert(compacted.end(), new_points.begin(), new_points.end());
+ }
+
+ if (static_cast(compacted.size()) >= kMinTrackedPoints) return compacted;
+
+ for (auto y = kGridStep / 2; y < gray.rows; y += kGridStep) {
+ for (auto x = kGridStep / 2; x < gray.cols; x += kGridStep) {
+ auto point = cv::Point2f { static_cast(x), static_cast(y) };
+ if (!mask_allows(allowed_mask, point)) continue;
+
+ compacted.push_back(point);
+ if (static_cast(compacted.size()) >= kMaxFeatures) return compacted;
+ }
+ }
+ return compacted;
+ }
+
+ static auto into_original_points(const std::vector& points)
+ -> std::vector {
+ auto result = std::vector { };
+ result.reserve(points.size());
+
+ constexpr auto inv_scale = 1.0 / kScale;
+ for (const auto& point : points) {
+ result.emplace_back(cv::Point2d { point.x * inv_scale, point.y * inv_scale });
+ }
+ return result;
+ }
+
+ auto configure_camera_matrix(std::array input) -> void {
+ camera_matrix = cv::Mat(3, 3, CV_64F);
+ for (auto i = 0; i < 9; ++i) {
+ camera_matrix.at(i / 3, i % 3) = input[static_cast(i)];
+ }
+ }
+
+ auto configure_distort_coeff(std::array input) -> void {
+ distort_coeff = cv::Mat(1, 5, CV_64F);
+ for (auto i = 0; i < 5; ++i) {
+ distort_coeff.at(0, i) = input[static_cast(i)];
+ }
+ }
+
+ auto reset() -> void {
+ prev_gray.release();
+ armor_mask.release();
+ lightbar_mask.release();
+ prev_points.clear();
+ prev_timestamp = { };
+ addition = { };
+ }
+
+ auto update_mask(std::span armors) -> void {
+ if (prev_gray.empty()) return;
+
+ auto mask = cv::Mat { prev_gray.size(), CV_8UC1, cv::Scalar { 255 } };
+ for (const auto& armor : armors) {
+ auto points = armor.points();
+ for (auto& p : points) {
+ p.x *= static_cast(kScale);
+ p.y *= static_cast(kScale);
+ }
+
+ auto rect = cv::boundingRect(points);
+ {
+ const auto pad = static_cast(
+ std::lround(std::max(rect.width, rect.height) * kMaskPaddingRatio));
+ rect.x -= pad;
+ rect.y -= pad;
+ rect.width += pad * 2;
+ rect.height += pad * 2;
+ rect &= cv::Rect { 0, 0, mask.cols, mask.rows };
+ }
+ if (rect.area() > 0) cv::rectangle(mask, rect, cv::Scalar { 0 }, cv::FILLED);
+ }
+ armor_mask = mask;
+ }
+
+ auto update_mask(std::span lightbars) -> void {
+ if (prev_gray.empty()) return;
+
+ auto mask = cv::Mat { prev_gray.size(), CV_8UC1, cv::Scalar { 255 } };
+ for (const auto& bar : lightbars) {
+ const auto ux = static_cast(bar.upper.x * kScale);
+ const auto uy = static_cast(bar.upper.y * kScale);
+ const auto lx = static_cast(bar.lower.x * kScale);
+ const auto ly = static_cast(bar.lower.y * kScale);
+
+ auto points = std::vector {
+ cv::Point2f { ux, uy },
+ cv::Point2f { lx, ly },
+ };
+
+ auto rect = cv::boundingRect(points);
+ {
+ const auto pad = static_cast(
+ std::lround(std::max(rect.width, rect.height) * kMaskPaddingRatio));
+ rect.x -= pad;
+ rect.y -= pad;
+ rect.width += pad * 2;
+ rect.height += pad * 2;
+ rect &= cv::Rect { 0, 0, mask.cols, mask.rows };
+ }
+ if (rect.area() > 0) cv::rectangle(mask, rect, cv::Scalar { 0 }, cv::FILLED);
+ }
+ lightbar_mask = mask;
+ }
+
+ auto update(const Image& image) -> std::optional {
+
+ const auto& input = image.details().mat;
+ if (input.empty()) return std::nullopt;
+
+ auto raw_gray = cv::Mat { };
+ if (input.channels() == 1) {
+ raw_gray = input.clone();
+ } else {
+ cv::cvtColor(input, raw_gray, cv::COLOR_BGR2GRAY);
+ }
+
+ auto gray = cv::Mat { };
+ cv::resize(raw_gray, gray, cv::Size { }, kScale, kScale, cv::INTER_AREA);
+
+ auto allowed_mask = cv::Mat { gray.size(), CV_8UC1, cv::Scalar { 255 } };
+ if (!armor_mask.empty()) {
+ cv::bitwise_and(allowed_mask, armor_mask, allowed_mask);
+ }
+ if (!lightbar_mask.empty()) {
+ cv::bitwise_and(allowed_mask, lightbar_mask, allowed_mask);
+ }
+
+ const auto timestamp = image.get_timestamp();
+
+ addition = { };
+ addition.timestamp = timestamp;
+ addition.scale = kScale;
+
+ if (prev_gray.empty()) {
+ prev_points = detect_features(gray, allowed_mask, { });
+ prev_gray = gray;
+ prev_timestamp = timestamp;
+ addition.feature_points = static_cast(prev_points.size());
+ return std::nullopt;
+ }
+
+ const auto dt = std::chrono::duration(timestamp - prev_timestamp).count();
+ addition.dt = dt;
+ if (!std::isfinite(dt) || dt < kMinDt || dt > kMaxDt || prev_points.empty()) {
+ prev_points = detect_features(gray, allowed_mask, { });
+ prev_gray = gray;
+ prev_timestamp = timestamp;
+ addition.feature_points = static_cast(prev_points.size());
+ return std::nullopt;
+ }
+
+ const auto tracked = track(prev_gray, gray, allowed_mask);
+ const auto tracked_points = static_cast(tracked.prev.size());
+ addition.tracked_points = tracked_points;
+ addition.inlier_points = static_cast(tracked.current_inliers.size());
+ addition.tracked_features = into_original_points(tracked.current_inliers);
+
+ const auto estimate = estimate_rotation(tracked, timestamp, dt, tracked_points);
+ if (estimate) {
+ addition.valid = true;
+ addition.confidence = estimate->confidence;
+ }
+
+ const auto next_points = estimate ? tracked.current_inliers : std::vector { };
+ if (static_cast(next_points.size()) >= kRefillThreshold) {
+ prev_points = next_points;
+ } else {
+ prev_points = detect_features(gray, allowed_mask, next_points);
+ }
+ prev_gray = gray;
+ prev_timestamp = timestamp;
+ addition.feature_points = static_cast(prev_points.size());
+
+ return estimate;
+ }
+
+ auto track(const cv::Mat& from, const cv::Mat& to, const cv::Mat& mask) const -> Tracks {
+ auto result = Tracks { };
+ if (prev_points.empty()) return result;
+
+ auto next_points = std::vector { };
+ auto status = std::vector { };
+ auto error = std::vector { };
+ cv::calcOpticalFlowPyrLK(from, to, prev_points, next_points, status, error,
+ cv::Size { 21, 21 }, 3,
+ cv::TermCriteria { cv::TermCriteria::COUNT | cv::TermCriteria::EPS, 30, 0.01 });
+
+ for (auto i = std::size_t { 0 }; i < next_points.size(); ++i) {
+ if (!status[i]) continue;
+ if (i < error.size() && error[i] > kMaxOpticalFlowErr) continue;
+ if (!in_border(prev_points[i], from.size()) || !in_border(next_points[i], to.size()))
+ continue;
+ if (!mask_allows(mask, prev_points[i]) || !mask_allows(mask, next_points[i])) continue;
+
+ result.prev.push_back(prev_points[i]);
+ result.current.push_back(next_points[i]);
+ }
+
+ if (result.prev.size() >= kMinTrackedPoints) {
+ auto inliers = std::vector { };
+ auto affine = cv::estimateAffinePartial2D(
+ result.prev, result.current, inliers, cv::RANSAC, 3.0, 2'000, 0.99, 10);
+
+ if (!affine.empty()) {
+ const auto angle = std::atan2(affine.at(1, 0), affine.at(0, 0));
+ result.roll_rate_like = angle;
+ }
+
+ for (auto i = std::size_t { 0 }; i < inliers.size(); ++i) {
+ if (!inliers[i]) continue;
+
+ result.prev_inliers.push_back(result.prev[i]);
+ result.current_inliers.push_back(result.current[i]);
+ }
+
+ if (result.prev_inliers.size() < kMinInlierPoints) {
+ result.prev_inliers = result.prev;
+ result.current_inliers = result.current;
+ result.roll_rate_like = 0.0;
+ }
+ }
+
+ return result;
+ }
+
+ auto estimate_rotation(const Tracks& tracks, TimePoint timestamp, double dt,
+ int tracked_points) const -> std::optional {
+ if (tracked_points < kMinTrackedPoints
+ || static_cast(tracks.current_inliers.size()) < kMinInlierPoints) {
+ return std::nullopt;
+ }
+
+ if (camera_matrix.empty() || camera_matrix.rows != 3 || camera_matrix.cols != 3
+ || camera_matrix.type() != CV_64F) {
+ return std::nullopt;
+ }
+
+ const auto fx = camera_matrix.at(0, 0) * kScale;
+ const auto fy = camera_matrix.at(1, 1) * kScale;
+ const auto cx = camera_matrix.at(0, 2) * kScale;
+ const auto cy = camera_matrix.at(1, 2) * kScale;
+ if (!std::isfinite(fx) || !std::isfinite(fy) || std::abs(fx) < 1e-6
+ || std::abs(fy) < 1e-6) {
+ return std::nullopt;
+ }
+
+ auto xs = std::vector { };
+ auto ys = std::vector { };
+ xs.reserve(tracks.current_inliers.size());
+ ys.reserve(tracks.current_inliers.size());
+
+ for (auto i = std::size_t { 0 }; i < tracks.current_inliers.size(); ++i) {
+ const auto prev_x = (tracks.prev_inliers[i].x - cx) / fx;
+ const auto prev_y = (tracks.prev_inliers[i].y - cy) / fy;
+ const auto current_x = (tracks.current_inliers[i].x - cx) / fx;
+ const auto current_y = (tracks.current_inliers[i].y - cy) / fy;
+
+ xs.push_back((current_x - prev_x) / dt);
+ ys.push_back((current_y - prev_y) / dt);
+ }
+
+ const auto inlier_points = static_cast(tracks.current_inliers.size());
+ const auto inlier_ratio =
+ tracked_points > 0 ? static_cast(inlier_points) / tracked_points : 0.0;
+ const auto count_score = std::clamp(static_cast(inlier_points) / 80.0, 0.0, 1.0);
+
+ return Estimate {
+ .timestamp = timestamp,
+ .dt = dt,
+ .yaw_rate = -median(std::move(xs)),
+ .pitch_rate = +median(std::move(ys)),
+ .roll_rate = tracks.roll_rate_like / dt,
+ .confidence = std::clamp(count_score * inlier_ratio, 0.0, 1.0),
+ };
+ }
+};
+
+auto RotationEstimator::configure(std::array camera_matrix) -> void {
+ pimpl->configure_camera_matrix(camera_matrix);
+}
+
+auto RotationEstimator::configure(std::array distort_coeff) -> void {
+ pimpl->configure_distort_coeff(distort_coeff);
+}
+
+auto RotationEstimator::update(const Image& image) -> std::optional {
+ return pimpl->update(image);
+}
+
+auto RotationEstimator::update_mask(std::span armors) -> void {
+ pimpl->update_mask(armors);
+}
+
+auto RotationEstimator::update_mask(std::span lightbars) -> void {
+ pimpl->update_mask(lightbars);
+}
+
+auto RotationEstimator::reset() -> void { pimpl->reset(); }
+
+auto RotationEstimator::addition() const -> const Addition& { return pimpl->addition; }
+
+RotationEstimator::RotationEstimator() noexcept
+ : pimpl { std::make_unique() } { }
+
+RotationEstimator::~RotationEstimator() noexcept = default;
diff --git a/src/module/perception/rotation_estimator.hpp b/src/module/perception/rotation_estimator.hpp
new file mode 100644
index 0000000..994f6ae
--- /dev/null
+++ b/src/module/perception/rotation_estimator.hpp
@@ -0,0 +1,72 @@
+#pragma once
+#include "utility/clock.hpp"
+#include "utility/image/image.hpp"
+#include "utility/pimpl.hpp"
+#include "utility/robot/armor.hpp"
+
+#include
+#include
+#include
+#include
+
+namespace rmcs {
+
+// 这是一段示例代码,用于发布估计的角加速度
+//
+// if (auto result = rotation_estimator.update(*image)) {
+// const auto& addition = rotation_estimator.addition();
+// for (const auto& point : addition.tracked_features) {
+// visual.draw_later(Canvas::Point {
+// .origin = point.make(),
+// .radius = 1,
+// .color = kGreen,
+// });
+// }
+// visual.publish(-result->yaw_rate, "estimate_yaw");
+// visual.publish(-result->pitch_rate, "estimate_pitch");
+// }
+
+class RotationEstimator {
+ RMCS_PIMPL_DEFINITION(RotationEstimator)
+
+public:
+ struct Estimate {
+ TimePoint timestamp;
+ double dt;
+
+ double yaw_rate;
+ double pitch_rate;
+ double roll_rate;
+
+ double confidence;
+ };
+ struct Addition {
+ TimePoint timestamp { };
+ double dt { 0.0 };
+
+ bool valid { false };
+
+ int tracked_points { 0 };
+ int inlier_points { 0 };
+ int feature_points { 0 };
+
+ double scale { 1.0 };
+ double confidence { 0.0 };
+
+ std::vector tracked_features;
+ };
+
+ auto configure(std::array camera_matrix) -> void;
+ auto configure(std::array distort_coeff) -> void;
+
+ auto update(const Image& image) -> std::optional;
+
+ auto update_mask(std::span armors) -> void;
+ auto update_mask(std::span lightbars) -> void;
+
+ auto addition() const -> const Addition&;
+
+ auto reset() -> void;
+};
+
+}
diff --git a/src/utility/image/drawable.cpp b/src/utility/image/drawable.cpp
index ef16ae0..350234e 100644
--- a/src/utility/image/drawable.cpp
+++ b/src/utility/image/drawable.cpp
@@ -99,4 +99,9 @@ auto Canvas::draw(const Text& text) -> void {
cv::putText(mat, text.content, org, font, scale, font_color, line_thickness, cv::LINE_AA);
}
+auto Canvas::draw(const Point& point) -> void {
+ auto& mat = canvas.details().mat;
+ cv::circle(mat, point.origin, point.radius, point.color, -1, cv::LINE_AA);
+}
+
}
diff --git a/src/utility/image/drawable.hpp b/src/utility/image/drawable.hpp
index 2d16e2e..6ece909 100644
--- a/src/utility/image/drawable.hpp
+++ b/src/utility/image/drawable.hpp
@@ -19,17 +19,23 @@ inline const auto kOrange = cv::Scalar { 000, 165, 255, 255 };
inline const auto kPurple = cv::Scalar { 128, 000, 128, 255 };
inline const auto kPink = cv::Scalar { 203, 192, 255, 255 };
-struct Text {
- std::string content;
- cv::Point2i top_left;
- cv::Scalar color = kWhite;
-};
-struct Area {
- cv::Rect2i rect;
- cv::Scalar color = kWhite;
-};
-
struct Canvas {
+ struct Text {
+ std::string content;
+ cv::Point2i top_left;
+ cv::Scalar color = kWhite;
+ };
+ struct Area {
+ cv::Rect2i rect;
+ cv::Scalar color = kWhite;
+ };
+ struct Point {
+ cv::Point2i origin;
+ int radius;
+
+ cv::Scalar color = kWhite;
+ };
+
Image& canvas;
std::uint8_t transparency = 255;
@@ -39,6 +45,7 @@ struct Canvas {
auto draw(const Lightbar2d&) -> void;
auto draw(const cv::Rect2i&) -> void;
auto draw(const Text&) -> void;
+ auto draw(const Point&) -> void;
};
template
concept drawable_trait = requires(Canvas& canvas) { canvas.draw(std::declval()); };
diff --git a/src/utility/math/linear.hpp b/src/utility/math/linear.hpp
index f767f33..ef9f81a 100644
--- a/src/utility/math/linear.hpp
+++ b/src/utility/math/linear.hpp
@@ -264,4 +264,6 @@ struct Transform {
}
};
+constexpr auto kNaN = std::numeric_limits::quiet_NaN();
+
}
diff --git a/src/utility/rclcpp/visual/scalar.cpp b/src/utility/rclcpp/visual/scalar.cpp
new file mode 100644
index 0000000..f4cbdc4
--- /dev/null
+++ b/src/utility/rclcpp/visual/scalar.cpp
@@ -0,0 +1,40 @@
+#include "scalar.hpp"
+#include "utility/rclcpp/node.details.hpp"
+
+#include
+
+using namespace rmcs::util::visual;
+
+using Float64 = std_msgs::msg::Float64;
+
+struct Scalar::Impl {
+ static inline rclcpp::Clock rclcpp_clock { RCL_STEADY_TIME };
+
+ RclcppNode& node;
+ std::string name;
+ std::shared_ptr> publisher;
+
+ explicit Impl(RclcppNode& node, std::string name) noexcept
+ : node { node }
+ , name { std::move(name) } { }
+
+ auto publish(double value) -> void {
+ if (!publisher) {
+ auto topic_name = node.get_pub_topic_prefix() + name;
+ publisher = node.details->make_pub(topic_name, qos::debug);
+ }
+ auto msg = Float64 {};
+ msg.data = value;
+ publisher->publish(msg);
+ }
+};
+
+Scalar::Scalar(RclcppNode& node, std::string name)
+ : pimpl { std::make_unique(node, std::move(name)) } { }
+
+Scalar::~Scalar() noexcept = default;
+
+Scalar::Scalar(Scalar&&) noexcept = default;
+auto Scalar::operator=(Scalar&&) noexcept -> Scalar& = default;
+
+auto Scalar::publish(double value) -> void { pimpl->publish(value); }
diff --git a/src/utility/rclcpp/visual/scalar.hpp b/src/utility/rclcpp/visual/scalar.hpp
new file mode 100644
index 0000000..6edaeec
--- /dev/null
+++ b/src/utility/rclcpp/visual/scalar.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "utility/pimpl.hpp"
+#include "utility/rclcpp/node.hpp"
+
+namespace rmcs::util::visual {
+
+struct Scalar {
+ RMCS_PIMPL_DEFINITION(Scalar)
+
+public:
+ Scalar(RclcppNode&, std::string name);
+
+ Scalar(Scalar&&) noexcept;
+ Scalar& operator=(Scalar&&) noexcept;
+
+ auto publish(double value) -> void;
+};
+
+}
diff --git a/src/utility/shared/context.hpp b/src/utility/shared/context.hpp
index ea3e2f3..6c27f67 100644
--- a/src/utility/shared/context.hpp
+++ b/src/utility/shared/context.hpp
@@ -8,14 +8,7 @@
namespace rmcs {
-template
-concept context_trait = std::is_trivially_copyable_v;
-
struct AutoAimState {
- static constexpr auto kLabel = "/shm_autoaim_state";
- static constexpr auto kLength = 512;
- static constexpr auto kNaN = std::numeric_limits::quiet_NaN();
-
TimePoint timestamp { };
bool should_control { false };
@@ -39,15 +32,10 @@ struct AutoAimState {
};
}
};
-static_assert(context_trait);
struct SystemContext {
using RobotId = rmcs_msgs::RobotId;
- static constexpr auto kLabel = "/shm_control_state";
- static constexpr auto kLength = 512;
- static constexpr auto kNaN = std::numeric_limits::quiet_NaN();
-
/// Dynamic Context
///
TimePoint timestamp { };
@@ -79,6 +67,5 @@ struct SystemContext {
};
}
};
-static_assert(context_trait);
} // namespace rmcs
diff --git a/tool/cxx/CMakeLists.txt b/tool/cxx/CMakeLists.txt
index 8d47af9..107fc37 100644
--- a/tool/cxx/CMakeLists.txt
+++ b/tool/cxx/CMakeLists.txt
@@ -119,6 +119,21 @@ if(HIKCAMERA_AVAILABLE)
${OpenCV_LIBS}
${hikcamera_LIBRARIES}
)
+
+ # Rotation Estimator
+ add_executable(
+ rotation_estimator
+ ${TOOL_DIR}/rotation_estimator.cpp
+ ${RMCS_SRC_DIR}/module/perception/rotation_estimator.cpp
+ ${RMCS_SRC_DIR}/utility/image/image.cpp
+ )
+ target_link_libraries(
+ rotation_estimator
+ rclcpp::rclcpp
+ ${geometry_msgs_LIBRARIES}
+ ${OpenCV_LIBS}
+ ${hikcamera_LIBRARIES}
+ )
else()
message(STATUS "hikcamera 未检测到,跳过相关构建")
endif()
diff --git a/tool/cxx/rotation_estimator.cpp b/tool/cxx/rotation_estimator.cpp
new file mode 100644
index 0000000..b8b2875
--- /dev/null
+++ b/tool/cxx/rotation_estimator.cpp
@@ -0,0 +1,219 @@
+#include "module/perception/rotation_estimator.hpp"
+#include "utility/image/image.details.hpp"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+namespace {
+
+using RotationMessage = geometry_msgs::msg::Vector3Stamped;
+
+struct LatestFrame {
+ std::mutex mutex;
+ std::condition_variable cv;
+
+ std::unique_ptr image;
+ std::uint64_t sequence { 0 };
+};
+
+struct Statistics {
+ std::atomic captured { 0 };
+ std::atomic dropped { 0 };
+ std::atomic read_time_sum_ms { 0.0 };
+
+ std::size_t processed { 0 };
+ std::size_t estimated { 0 };
+ double update_time_sum_ms { 0.0 };
+
+ int tracked_points { 0 };
+ int inlier_points { 0 };
+ double confidence { 0.0 };
+
+ auto add_read_time(std::chrono::steady_clock::duration duration) -> void {
+ read_time_sum_ms.fetch_add(std::chrono::duration(duration).count(),
+ std::memory_order::relaxed);
+ }
+
+ auto add_update_time(std::chrono::steady_clock::duration duration) -> void {
+ update_time_sum_ms += std::chrono::duration(duration).count();
+ }
+
+ auto record_estimate(const rmcs::RotationEstimator::Addition& addition) -> void {
+ estimated += 1;
+ tracked_points = addition.tracked_points;
+ inlier_points = addition.inlier_points;
+ confidence = addition.confidence;
+ }
+
+ auto record_failed_estimate(const rmcs::RotationEstimator::Addition& addition) -> void {
+ tracked_points = addition.tracked_points;
+ inlier_points = addition.inlier_points;
+ confidence = 0.0;
+ }
+
+ auto print_and_reset() -> void {
+ const auto captured_count = captured.exchange(0, std::memory_order::relaxed);
+ const auto dropped_count = dropped.exchange(0, std::memory_order::relaxed);
+ const auto read_time_sum = read_time_sum_ms.exchange(0.0, std::memory_order::relaxed);
+
+ const auto read_divisor = captured_count == 0 ? 1.0 : static_cast(captured_count);
+ const auto update_divisor = processed == 0 ? 1.0 : static_cast(processed);
+
+ std::println("[rotation_estimator] captured={}, processed={}, estimates={}, dropped={}, "
+ "tracked={}, "
+ "inliers={}, confidence={:.2f}, read={:.2f}ms, update={:.2f}ms",
+ captured_count, processed, estimated, dropped_count, tracked_points, inlier_points,
+ confidence, read_time_sum / read_divisor, update_time_sum_ms / update_divisor);
+
+ processed = 0;
+ estimated = 0;
+ update_time_sum_ms = 0.0;
+ }
+};
+
+constexpr auto kCameraMatrix = std::array {
+ 1.722231837421459e+03,
+ 0.0,
+ 7.013056440882832e+02,
+ 0.0,
+ 1.724876404292754e+03,
+ 5.645821718351237e+02,
+ 0.0,
+ 0.0,
+ 1.0,
+};
+
+constexpr auto kDistortCoeff = std::array {
+ -0.064232403853946,
+ -0.087667493884102,
+ 0.0,
+ 0.0,
+ 0.792381808294582,
+};
+
+} // namespace
+
+auto main(int argc, char** argv) -> int {
+ rclcpp::init(argc, argv);
+
+ auto node = std::make_shared("rotation_estimator_tool");
+ auto publisher = node->create_publisher(
+ "/rmcs/auto_aim/visual_rotation_rate", rclcpp::QoS { 10 }.best_effort().keep_last(10));
+ auto clock = rclcpp::Clock { RCL_STEADY_TIME };
+
+ auto config = hikcamera::Config {
+ .timeout_ms = 2'000,
+ .exposure_us = 1'500,
+ .framerate = 120,
+ .fixed_framerate = true,
+ };
+ config.framerate = 165;
+
+ auto camera = hikcamera::Camera { };
+ camera.configure(config);
+ if (auto result = camera.connect(); !result) {
+ std::println("[rotation_estimator] Failed to connect camera: {}", result.error());
+ rclcpp::shutdown();
+ return 1;
+ }
+
+ auto estimator = rmcs::RotationEstimator { };
+ estimator.configure(kCameraMatrix);
+ estimator.configure(kDistortCoeff);
+
+ auto latest_frame = LatestFrame { };
+ auto statistics = Statistics { };
+
+ auto capture_thread = std::jthread { [&](const std::stop_token& stop) {
+ while (!stop.stop_requested() && rclcpp::ok()) {
+ const auto read_begin = std::chrono::steady_clock::now();
+ auto captured = camera.read_image_with_timestamp();
+ const auto read_end = std::chrono::steady_clock::now();
+ statistics.add_read_time(read_end - read_begin);
+
+ if (!captured) {
+ std::println("[rotation_estimator] Failed to read image: {}", captured.error());
+ continue;
+ }
+
+ auto image = std::make_unique();
+ image->details().set_mat(captured->mat.clone());
+ image->set_timestamp(captured->timestamp);
+
+ {
+ auto lock = std::lock_guard { latest_frame.mutex };
+ if (latest_frame.image) statistics.dropped.fetch_add(1, std::memory_order::relaxed);
+ latest_frame.image = std::move(image);
+ latest_frame.sequence += 1;
+ }
+ statistics.captured.fetch_add(1, std::memory_order::relaxed);
+ latest_frame.cv.notify_one();
+ }
+ } };
+
+ auto last_print = std::chrono::steady_clock::now();
+ auto last_sequence = std::uint64_t { 0 };
+
+ std::println("[rotation_estimator] Publishing /rmcs/auto_aim/visual_rotation_rate");
+ std::println("[rotation_estimator] Press Ctrl+C to quit");
+
+ while (rclcpp::ok()) {
+ rclcpp::spin_some(node);
+
+ auto image = std::unique_ptr { };
+ {
+ auto lock = std::unique_lock { latest_frame.mutex };
+ latest_frame.cv.wait_for(lock, std::chrono::milliseconds { 5 },
+ [&] { return latest_frame.sequence != last_sequence || !rclcpp::ok(); });
+ if (latest_frame.sequence == last_sequence || !latest_frame.image) continue;
+
+ last_sequence = latest_frame.sequence;
+ image = std::move(latest_frame.image);
+ }
+
+ statistics.processed += 1;
+
+ const auto update_begin = std::chrono::steady_clock::now();
+ auto estimate = estimator.update(*image);
+ const auto update_end = std::chrono::steady_clock::now();
+ statistics.add_update_time(update_end - update_begin);
+ const auto& addition = estimator.addition();
+
+ if (estimate) {
+ statistics.record_estimate(addition);
+
+ auto message = RotationMessage { };
+ message.header.stamp = clock.now();
+ message.header.frame_id = "camera_link";
+ message.vector.x = estimate->yaw_rate;
+ message.vector.y = estimate->pitch_rate;
+ message.vector.z = estimate->roll_rate;
+ publisher->publish(message);
+ } else {
+ statistics.record_failed_estimate(addition);
+ }
+
+ const auto now = std::chrono::steady_clock::now();
+ if (now - last_print >= std::chrono::seconds { 1 }) {
+ statistics.print_and_reset();
+ last_print = now;
+ }
+ }
+
+ capture_thread.request_stop();
+ latest_frame.cv.notify_one();
+ if (capture_thread.joinable()) capture_thread.join();
+ std::ignore = camera.disconnect();
+ rclcpp::shutdown();
+ std::println("[rotation_estimator] Stopped");
+ return 0;
+}