Compare commits

2 Commits

Author SHA1 Message Date
681785f2f3 改bug 2026-03-21 20:15:56 +08:00
52301bcf8d solver.h & armor_finder.h: 新增了提取相机内参的接口,以及存储 2D 大框中心点 (target_area_center) 和过滤后的绝杀深度 (z_min_filtered)。
find_armor_box.cpp: 废弃了单一块板子的限制,将同一分类的板子全拿来算出包含自转幅度的巨型像素框正中点。
armor_finder.cpp: 让这辆车当前所有能看见的板子全部去跑 PnP 获得深度,取物理上离你最近的深度 $Z$,结合上一步的框中心点,逆推换算出一个堪称完美的 3D 虚拟准星点。
send_target.cpp: 彻底移除了容易抽搐的差值 PID 算法。现在直接根据虚拟准星算出最纯净的绝对偏航角(Yaw)和下坠角(Pitch)发给下位机,云台将彻底锁死!
auto_trigger.h: 抛弃容易受抖动破坏的三维测速。改用极其丝滑的二维图像像素帧差分测速:计算某一块板子滑到中心线刚好等于引力滞空飞行的时间,完美触发一击绝杀。
⚠️ 终极提醒(切记!切记!): 算法已经全部就位,但是这套神算法的基础基石——物理高度落差需要你手动填入。 请务必拿尺子去量一下车上摄像头和发弹孔的落差,修改 others/solver_config.yml 中的: t_camera2gimbal: [0, 0, 0] 为真实厘米数(例如 [0, -10, 5])。
2026-03-21 20:03:47 +08:00
7 changed files with 91 additions and 65 deletions

View File

@@ -109,7 +109,7 @@ private:
const uint8_t &enemy_color; // 敌方颜色,引用外部变量,自动变化 const uint8_t &enemy_color; // 敌方颜色,引用外部变量,自动变化
const uint8_t &is_anti_top; // 进入反陀螺,引用外部变量,自动变化 const uint8_t &is_anti_top; // 进入反陀螺,引用外部变量,自动变化
State state; // 自瞄状态对象实例 State state; // 自瞄状态对象实例
ArmorBox target_box, last_box; // 目标装甲板 ArmorBoxes current_target_boxes; // 聚类后的同一车体装甲板
int anti_switch_cnt; // 防止乱切目标计数器 int anti_switch_cnt; // 防止乱切目标计数器
cv::Ptr<cv::Tracker> tracker; // tracker对象实例 cv::Ptr<cv::Tracker> tracker; // tracker对象实例
Classifier classifier; // CNN分类器对象实例用于数字识别 Classifier classifier; // CNN分类器对象实例用于数字识别
@@ -142,6 +142,10 @@ private:
bool sendBoxPosition(uint16_t shoot_delay); // 发送装甲板位置 bool sendBoxPosition(uint16_t shoot_delay); // 发送装甲板位置
bool shouldFire() const { return can_fire; } // 获取开火建议 bool shouldFire() const { return can_fire; } // 获取开火建议
public: public:
ArmorBox target_box, last_box; // 目标装甲板
cv::Point2f target_area_center; // 目标大框像素中心
double z_min_filtered = 0; // 目标深度极小值
void run(cv::Mat &src); // 自瞄主函数 void run(cv::Mat &src); // 自瞄主函数
struct HistoryItem { struct HistoryItem {
cv::Point3f pos; cv::Point3f pos;

View File

@@ -17,6 +17,7 @@ class Solver
public: public:
explicit Solver(const std::string & config_path); explicit Solver(const std::string & config_path);
const cv::Mat& get_camera_matrix() const { return camera_matrix_; }
Eigen::Matrix3d R_gimbal2world() const; Eigen::Matrix3d R_gimbal2world() const;
void set_R_gimbal2world(const Eigen::Quaterniond & q); void set_R_gimbal2world(const Eigen::Quaterniond & q);

View File

@@ -21,40 +21,32 @@ public:
* @return true 目标与预测弹着点重合,建议击发 * @return true 目标与预测弹着点重合,建议击发
*/ */
static bool should_fire(const ArmorFinder &armor_finder, double v0, double gimbal_yaw, double gimbal_pitch, double threshold_deg = -1.0) { static bool should_fire(const ArmorFinder &armor_finder, double v0, double gimbal_yaw, double gimbal_pitch, double threshold_deg = -1.0) {
if (armor_finder.history.size() < 3) return false; // 轨迹点不足,不预测 if (armor_finder.last_box.rect == cv::Rect2d()) return false;
cv::Point2f curr_center = armor_finder.target_box.getCenter();
cv::Point2f last_center = armor_finder.last_box.getCenter();
cv::Point2f area_center = armor_finder.target_area_center;
// 1. 获取目标当前的 3D 状态 (相对于相机) // 横向速度估计 (采用帧差分,工业相机一般 ~100 fps)
const auto &latest = armor_finder.history.back(); double vx = curr_center.x - last_center.x;
double dist = latest.pos.z / 100.0; // cm -> m if (std::abs(vx) < 1.0) return false;
// 2. 估计目标速度 (简单线性回归或两点差分) // 计算这块装甲板滑到大框中心所需的预测帧数
const auto &first = armor_finder.history.front(); double frames_to_pass = (area_center.x - curr_center.x) / vx;
double dt = latest.time - first.time;
if (dt <= 0) return false; // 若不是朝着中心滑动 (已偏离),不激发
if (frames_to_pass < 0) return false;
cv::Point3f velocity = (latest.pos - first.pos) * (1.0 / dt); // cm/s
// 弹道时间预判
// 3. 计算飞行时间 (单位: s) double dist = armor_finder.z_min_filtered / 100.0; // cm -> m
double t_flight = BallisticSolver::get_flight_time(dist, -gimbal_pitch, v0, BALLISTIC_K); double t_flight = BallisticSolver::get_flight_time(dist, -gimbal_pitch, v0, BALLISTIC_K);
double t_total = t_flight + SYSTEM_DELAY / 1000.0; double t_total = t_flight + SYSTEM_DELAY / 1000.0;
// 4. 预测目标未来的位置 (cm) // 将总延迟时间换算为帧数偏移 (以 100FPS)
cv::Point3f pos_pred = latest.pos + velocity * t_total; double delay_frames = t_total * 100.0;
// 5. 将预测位置映射回像素坐标 (用于判断重合) // 如果剩余到达中心时间 = 飞弹+机械延迟时间 左右,开火!
double x_pred_pixel = pos_pred.x * FOCUS_PIXAL / pos_pred.z + IMAGE_CENTER_X; return std::abs(frames_to_pass - delay_frames) < 3.0; // 容差 +/- 3帧
double y_pred_pixel = pos_pred.y * FOCUS_PIXAL / pos_pred.z + IMAGE_CENTER_Y;
// 6. 推断弹着点偏移 (像素级)
// 理想打击点应该就在相机中心 (自瞄已经对准补差后的位置)
// 但是我们需要判断当前云台的 "指向误差" 是否足够小
double dist_to_center = sqrt(pow(x_pred_pixel - IMAGE_CENTER_X, 2) + pow(y_pred_pixel - IMAGE_CENTER_Y, 2));
// 7. 动态阈值判定 (依据目标距离和装甲板大小)
// 距离越远,允许的像素误差越小
double threshold = (threshold_deg > 0) ? (threshold_deg * FOCUS_PIXAL * PI / 180.0 / 100.0) : (200.0 / (dist + 0.001));
return dist_to_center < threshold;
} }
}; };

View File

@@ -114,19 +114,40 @@ end:
cv::waitKey(1); cv::waitKey(1);
} }
// 自动扳机位置预测逻辑:更新历史位置 // 自动扳机位置预测与虚拟靶点重推逻辑
if (target_box.rect != cv::Rect2d()) { if (target_box.rect != cv::Rect2d()) {
auto_aim::Armor armor; // 1. 遍历当前目标车的所有可见装甲板,寻找极小深度 Z_min
armor.type = (target_box.id == B1 || target_box.id == R1 || target_box.id == B2 || target_box.id == R2 || target_box.id == B7 || target_box.id == R7 || target_box.id == B8 || target_box.id == R8) ? auto_aim::ArmorType::big : auto_aim::ArmorType::small; double min_z = 99999.0;
armor.name = static_cast<auto_aim::ArmorName>(target_box.id);
armor.points = target_box.points;
// 更新云台位姿 (使用 MCU 回传的 yaw/pitch单位假设为度需转为弧度)
solver.set_R_gimbal2world(mcu_data.curr_yaw * CV_PI / 180.0, mcu_data.curr_pitch * CV_PI / 180.0); solver.set_R_gimbal2world(mcu_data.curr_yaw * CV_PI / 180.0, mcu_data.curr_pitch * CV_PI / 180.0);
for (auto &b : current_target_boxes) {
auto_aim::Armor arm;
arm.type = (b.id == B1 || b.id == R1 || b.id == B2 || b.id == R2 || b.id == B7 || b.id == R7 || b.id == B8 || b.id == R8) ? auto_aim::ArmorType::big : auto_aim::ArmorType::small;
arm.name = static_cast<auto_aim::ArmorName>(b.id);
arm.points = b.points;
solver.solve(arm);
double z_cm = arm.xyz_in_gimbal.z() * 100.0;
if (z_cm < min_z) min_z = z_cm;
}
solver.solve(armor); // 平滑滤波获取稳定的 z_min
if (z_min_filtered <= 0.1 || min_z < z_min_filtered) {
z_min_filtered = min_z; // 激进跟进最小值锁定
} else {
z_min_filtered = z_min_filtered * 0.7 + min_z * 0.3; // 缓慢恢复
}
// 2. 将 2D 大框中心逆推为 3D 虚拟靶点
const cv::Mat& cam_mat = solver.get_camera_matrix();
double fx = cam_mat.at<double>(0, 0);
double cx = cam_mat.at<double>(0, 2);
double fy = cam_mat.at<double>(1, 1);
double cy = cam_mat.at<double>(1, 2);
double X_cam = (target_area_center.x - cx) * z_min_filtered / fx;
double Y_cam = (target_area_center.y - cy) * z_min_filtered / fy;
target_xyz = cv::Point3f(X_cam, Y_cam, z_min_filtered);
target_xyz = cv::Point3f(armor.xyz_in_gimbal.x() * 100.0, armor.xyz_in_gimbal.y() * 100.0, armor.xyz_in_gimbal.z() * 100.0); // 转换为cm
cv::Point3f current_pos = target_xyz; cv::Point3f current_pos = target_xyz;
double current_time = frame_time / 1000.0; double current_time = frame_time / 1000.0;
history.push_back({current_pos, current_time}); history.push_back({current_pos, current_time});

View File

@@ -206,6 +206,26 @@ bool ArmorFinder::findArmorBox(const cv::Mat &src, ArmorBox &box) {
} else { // 如果分类器不可用,则直接选取候选区中的第一个区域作为目标(往往会误识别) } else { // 如果分类器不可用,则直接选取候选区中的第一个区域作为目标(往往会误识别)
box = armor_boxes[0]; box = armor_boxes[0];
} }
// --- 反小陀螺:同类装甲板大框聚类 ---
if (box.rect != cv::Rect2d(0, 0, 0, 0)) {
current_target_boxes.clear();
double min_x = 9999, max_x = -9999, min_y = 9999, max_y = -9999;
for (const auto &one_box : armor_boxes) {
// 将同分类 ID 的装甲板聚类 (即使未分类也可根据需求退化处理)
if (one_box.id == box.id) {
current_target_boxes.push_back(one_box);
min_x = fmin(min_x, one_box.rect.x);
max_x = fmax(max_x, one_box.rect.x + one_box.rect.width);
min_y = fmin(min_y, one_box.rect.y);
max_y = fmax(max_y, one_box.rect.y + one_box.rect.height);
}
}
if (!current_target_boxes.empty()) {
target_area_center = cv::Point2f(min_x + (max_x - min_x) / 2.0, min_y + (max_y - min_y) / 2.0);
}
}
return true; return true;
} }

View File

@@ -51,28 +51,9 @@ bool ArmorFinder::sendBoxPosition(uint16_t shoot_delay) {
if (shoot_delay) { if (shoot_delay) {
LOGM(STR_CTR(WORD_BLUE, "next box %dms"), shoot_delay); LOGM(STR_CTR(WORD_BLUE, "next box %dms"), shoot_delay);
} }
auto rect = target_box.rect;
double dx = rect.x + rect.width / 2 - IMAGE_CENTER_X; // 直接基于虚拟靶心 target_xyz (已包含 Z_min 和中心坐标转换) 算出绝对偏航角
double dy = rect.y + rect.height / 2 - IMAGE_CENTER_Y; double yaw = atan2(target_xyz.x, target_xyz.z) * 180 / PI;
// PID
sum_yaw += dx;
sum_pitch += dy;
float yaw_I_component = YAW_AIM_KI * sum_yaw;
float pitch_I_component = PITCH_AIM_KI * sum_pitch;
double tmp_yaw = dx;
double tmp_pitch = dy;
dx = YAW_AIM_KP * dx + YAW_AIM_KI * sum_yaw +
YAW_AIM_KD * (dx - last_yaw);
dy = PITCH_AIM_KP * dy + PITCH_AIM_KI * sum_pitch +
PITCH_AIM_KD * (dy - last_pitch);
last_yaw = tmp_yaw;
last_pitch = tmp_pitch;
//
double yaw = atan(dx / FOCUS_PIXAL) * 180 / PI;
double dist = sqrt(target_xyz.x * target_xyz.x + target_xyz.y * target_xyz.y + target_xyz.z * target_xyz.z) / 100.0; // 米 double dist = sqrt(target_xyz.x * target_xyz.x + target_xyz.y * target_xyz.y + target_xyz.z * target_xyz.z) / 100.0; // 米
// 弹道补偿使用 PnP 提供的 3D 坐标 // 弹道补偿使用 PnP 提供的 3D 坐标
@@ -80,9 +61,14 @@ bool ArmorFinder::sendBoxPosition(uint16_t shoot_delay) {
double y_target = -target_xyz.y / 100.0; // 垂直高度差 double y_target = -target_xyz.y / 100.0; // 垂直高度差
double pitch_comp = BallisticSolver::get_pitch(x_target, y_target, MUZZLE_VELOCITY, BALLISTIC_K); double pitch_comp = BallisticSolver::get_pitch(x_target, y_target, MUZZLE_VELOCITY, BALLISTIC_K);
// 计算是否满足开火条件 (例如残差小于 1.5 度) // 计算是否满足开火条件 (容差 1.5 度,可根据需要调节)
can_fire = AutoTrigger::should_fire(*this, MUZZLE_VELOCITY, yaw, pitch_comp, 1.5); can_fire = AutoTrigger::should_fire(*this, MUZZLE_VELOCITY, yaw, pitch_comp, 1.5);
// 当满足预测开火条件时,通知电控 (借用 shoot_delay 或其他预留位)
if (can_fire && shoot_delay == 0) {
shoot_delay = 1; // 假定 1 为开火 flag由电控在裁判系统中绘制绿框或自动激发
}
return sendTarget(serial, yaw, pitch_comp, dist * 100.0, shoot_delay); return sendTarget(serial, yaw, pitch_comp, dist * 100.0, shoot_delay);
} }

View File

@@ -4,7 +4,9 @@
#ifndef _SETCONFIG_H_ #ifndef _SETCONFIG_H_
#define _SETCONFIG_H_ #define _SETCONFIG_H_
#ifndef WITH_CONFIG
#define WITH_CONFIG #define WITH_CONFIG
#endif
#ifdef WITH_CONFIG #ifdef WITH_CONFIG
#include <config/config.h> #include <config/config.h>