Compare commits
10 Commits
7c07eb66c8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9590900658 | ||
|
|
6459f7c6f5 | ||
|
|
e0aa66dff8 | ||
|
|
14809af5f6 | ||
|
|
4ad21bba6a | ||
|
|
ed23cf5482 | ||
|
|
0802d430a5 | ||
|
|
caaa18d30d | ||
|
|
4f4715fb82 | ||
|
|
85598eaa51 |
@@ -26,6 +26,8 @@ add_executable(nav_lite_node
|
|||||||
src/local_planner.cpp
|
src/local_planner.cpp
|
||||||
src/bt_executor.cpp
|
src/bt_executor.cpp
|
||||||
src/session_mapper.cpp
|
src/session_mapper.cpp
|
||||||
|
src/cone_updater.cpp
|
||||||
|
src/laser_safety.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(nav_lite_node PRIVATE
|
target_include_directories(nav_lite_node PRIVATE
|
||||||
@@ -42,6 +44,9 @@ add_executable(calib_check src/calib_check.cpp)
|
|||||||
target_include_directories(calib_check PRIVATE ${INCLUDE_DIR} ${EIGEN3_INCLUDE_DIRS})
|
target_include_directories(calib_check PRIVATE ${INCLUDE_DIR} ${EIGEN3_INCLUDE_DIRS})
|
||||||
ament_target_dependencies(calib_check rclcpp geometry_msgs nav_msgs tf2 tf2_ros)
|
ament_target_dependencies(calib_check rclcpp geometry_msgs nav_msgs tf2 tf2_ros)
|
||||||
install(TARGETS nav_lite_node calib_check DESTINATION lib/${PROJECT_NAME})
|
install(TARGETS nav_lite_node calib_check DESTINATION lib/${PROJECT_NAME})
|
||||||
install(DIRECTORY launch config maps DESTINATION share/${PROJECT_NAME})
|
install(DIRECTORY launch config DESTINATION share/${PROJECT_NAME})
|
||||||
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/maps")
|
||||||
|
install(DIRECTORY maps DESTINATION share/${PROJECT_NAME})
|
||||||
|
endif()
|
||||||
|
|
||||||
ament_package()
|
ament_package()
|
||||||
|
|||||||
38
include/car_nav_lite/cone_updater.hpp
Normal file
38
include/car_nav_lite/cone_updater.hpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <Eigen/Dense>
|
||||||
|
#include "car_nav_lite/types.hpp"
|
||||||
|
#include "car_nav_lite/fence_model.hpp"
|
||||||
|
|
||||||
|
namespace car_nav_lite {
|
||||||
|
|
||||||
|
class ConeUpdater {
|
||||||
|
public:
|
||||||
|
ConeUpdater();
|
||||||
|
|
||||||
|
// Process one scan frame: extract cone candidates, track across frames.
|
||||||
|
// confirmed_cones is the current known cone list (for dedup).
|
||||||
|
void update(const LaserScan& scan, const Pose2D& pose,
|
||||||
|
const FenceModel& fence,
|
||||||
|
const std::vector<Eigen::Vector2d>& known_cones);
|
||||||
|
|
||||||
|
// Returns newly confirmed cones and clears them from pending.
|
||||||
|
std::vector<Eigen::Vector2d> popConfirmed();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Candidate {
|
||||||
|
Eigen::Vector2d center;
|
||||||
|
int hits = 0;
|
||||||
|
int age = 0;
|
||||||
|
};
|
||||||
|
std::vector<Candidate> candidates_;
|
||||||
|
|
||||||
|
static constexpr int CONFIRM_HITS = 3;
|
||||||
|
static constexpr double CLUSTER_DIST = 0.10;
|
||||||
|
static constexpr double MERGE_DIST = 0.25;
|
||||||
|
static constexpr double WALL_DIST = 0.15;
|
||||||
|
static constexpr int MIN_POINTS = 4;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace car_nav_lite
|
||||||
@@ -35,6 +35,9 @@ struct FenceModel {
|
|||||||
// Pre-computed wall positions for localization
|
// Pre-computed wall positions for localization
|
||||||
double left_x = -2.5, right_x = 2.5;
|
double left_x = -2.5, right_x = 2.5;
|
||||||
double bottom_y = -2.5, top_y = 2.5;
|
double bottom_y = -2.5, top_y = 2.5;
|
||||||
|
// Pre-computed corner points for drawing rotated fence
|
||||||
|
double p_lb_x=0, p_lb_y=0, p_lt_x=0, p_lt_y=0;
|
||||||
|
double p_rb_x=0, p_rb_y=0, p_rt_x=0, p_rt_y=0;
|
||||||
double yaw_field = 0.0;
|
double yaw_field = 0.0;
|
||||||
double width = 0.0, height = 0.0;
|
double width = 0.0, height = 0.0;
|
||||||
double cx = 0.0, cy = 0.0;
|
double cx = 0.0, cy = 0.0;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "car_nav_lite/types.hpp"
|
#include "car_nav_lite/types.hpp"
|
||||||
|
#include "car_nav_lite/fence_model.hpp"
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
@@ -50,10 +51,12 @@ public:
|
|||||||
// ── Layered map (fence + cone + dynamic) ──
|
// ── Layered map (fence + cone + dynamic) ──
|
||||||
void freezeSessionMap();
|
void freezeSessionMap();
|
||||||
void composeLayers();
|
void composeLayers();
|
||||||
|
void decayDynamic(uint8_t amount = 10);
|
||||||
|
|
||||||
// ── Pre-built obstacle marks ───────────────────────
|
// ── Pre-built obstacle marks ───────────────────────
|
||||||
void markCircle(double wx, double wy, double radius, uint8_t value = 255);
|
void markCircle(double wx, double wy, double radius, uint8_t value = 255);
|
||||||
void markCone(double wx, double wy, double radius, uint8_t value = 255);
|
void markCone(double wx, double wy, double radius, uint8_t value = 255);
|
||||||
|
void markFenceModel(const FenceModel& fence, double thickness = 0.04);
|
||||||
void markRect(double x1, double y1, double x2, double y2, uint8_t value = 255);
|
void markRect(double x1, double y1, double x2, double y2, uint8_t value = 255);
|
||||||
|
|
||||||
// ── Getters ────────────────────────────────────────
|
// ── Getters ────────────────────────────────────────
|
||||||
|
|||||||
20
include/car_nav_lite/laser_safety.hpp
Normal file
20
include/car_nav_lite/laser_safety.hpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "car_nav_lite/types.hpp"
|
||||||
|
|
||||||
|
namespace car_nav_lite {
|
||||||
|
|
||||||
|
// Fast scan-based safety filter. Runs every frame before publishing cmd_vel.
|
||||||
|
// Checks if the planned command would hit anything in the latest scan.
|
||||||
|
struct LaserSafetyFilter {
|
||||||
|
bool enable = true;
|
||||||
|
double front_width = 0.30; // half-width of front safety zone (m)
|
||||||
|
double stop_dist = 0.35; // brake distance (m)
|
||||||
|
double slow_dist = 0.70; // slow-down distance (m)
|
||||||
|
double turn_wz = 0.8; // evasive turn rate (rad/s)
|
||||||
|
double max_range = 2.0; // max lookahead (m)
|
||||||
|
|
||||||
|
Twist filter(const Twist& cmd, const LaserScan& scan) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace car_nav_lite
|
||||||
@@ -24,7 +24,7 @@ public:
|
|||||||
// Metrics
|
// Metrics
|
||||||
int loc_lines_ = 0;
|
int loc_lines_ = 0;
|
||||||
double loc_dx_ = 0, loc_dy_ = 0, loc_dyaw_ = 0;
|
double loc_dx_ = 0, loc_dy_ = 0, loc_dyaw_ = 0;
|
||||||
double loc_conf_ = 1.0;
|
double loc_conf_ = 1.0, loc_rms_ = 999.0;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Pose2D odom_pose_;
|
Pose2D odom_pose_;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public:
|
|||||||
double expected_size = 5.0, double size_tol = 0.35,
|
double expected_size = 5.0, double size_tol = 0.35,
|
||||||
double ransac_thresh = 0.04);
|
double ransac_thresh = 0.04);
|
||||||
|
|
||||||
|
void reset() { points_world_.clear(); scan_count_ = 0; }
|
||||||
int scanCount() const { return scan_count_; }
|
int scanCount() const { return scan_count_; }
|
||||||
const std::vector<Eigen::Vector2d>& points() const { return points_world_; }
|
const std::vector<Eigen::Vector2d>& points() const { return points_world_; }
|
||||||
|
|
||||||
|
|||||||
114
src/cone_updater.cpp
Normal file
114
src/cone_updater.cpp
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#include "car_nav_lite/cone_updater.hpp"
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace car_nav_lite {
|
||||||
|
|
||||||
|
ConeUpdater::ConeUpdater() {}
|
||||||
|
|
||||||
|
void ConeUpdater::update(const LaserScan& scan, const Pose2D& pose,
|
||||||
|
const FenceModel& fence,
|
||||||
|
const std::vector<Eigen::Vector2d>& known_cones) {
|
||||||
|
if (!fence.valid) return;
|
||||||
|
|
||||||
|
// ── 1. Extract non-wall, non-known-cone world points ──
|
||||||
|
std::vector<Eigen::Vector2d> pts;
|
||||||
|
double cy = std::cos(pose.yaw), sy = std::sin(pose.yaw);
|
||||||
|
for (int i = 0; i < scan.size(); i += 3) {
|
||||||
|
double r = scan.ranges[i];
|
||||||
|
if (r < 0.15 || r > 4.5) continue;
|
||||||
|
double a = scan.angle_min + i * scan.angle_increment;
|
||||||
|
double wx = pose.x + r * std::cos(a + pose.yaw);
|
||||||
|
double wy = pose.y + r * std::sin(a + pose.yaw);
|
||||||
|
|
||||||
|
// Skip wall points
|
||||||
|
double dw = std::min({
|
||||||
|
fence.left.distance(wx, wy), fence.right.distance(wx, wy),
|
||||||
|
fence.bottom.distance(wx, wy), fence.top.distance(wx, wy)
|
||||||
|
});
|
||||||
|
if (dw < WALL_DIST) continue;
|
||||||
|
|
||||||
|
// Skip near known cones
|
||||||
|
bool near_cone = false;
|
||||||
|
for (auto& c : known_cones) {
|
||||||
|
double dx = wx - c.x(), dy = wy - c.y();
|
||||||
|
if (dx*dx + dy*dy < MERGE_DIST * MERGE_DIST) { near_cone = true; break; }
|
||||||
|
}
|
||||||
|
if (near_cone) continue;
|
||||||
|
|
||||||
|
pts.emplace_back(wx, wy);
|
||||||
|
}
|
||||||
|
if (pts.size() < 10) return;
|
||||||
|
|
||||||
|
// ── 2. Simple grid clustering ──
|
||||||
|
std::vector<std::vector<Eigen::Vector2d>> clusters;
|
||||||
|
std::vector<bool> used(pts.size(), false);
|
||||||
|
for (size_t i = 0; i < pts.size(); ++i) {
|
||||||
|
if (used[i]) continue;
|
||||||
|
std::vector<Eigen::Vector2d> cl;
|
||||||
|
cl.push_back(pts[i]); used[i] = true;
|
||||||
|
for (size_t j = 0; j < cl.size(); ++j) {
|
||||||
|
for (size_t k = i+1; k < pts.size(); ++k) {
|
||||||
|
if (used[k]) continue;
|
||||||
|
double dx = pts[k].x() - cl[j].x();
|
||||||
|
double dy = pts[k].y() - cl[j].y();
|
||||||
|
if (dx*dx + dy*dy < CLUSTER_DIST * CLUSTER_DIST) {
|
||||||
|
cl.push_back(pts[k]); used[k] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cl.size() >= MIN_POINTS) clusters.push_back(cl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3. Match clusters to candidates ──
|
||||||
|
for (auto& cl : clusters) {
|
||||||
|
Eigen::Vector2d center(0, 0);
|
||||||
|
for (auto& p : cl) { center.x() += p.x(); center.y() += p.y(); }
|
||||||
|
center /= (double)cl.size();
|
||||||
|
|
||||||
|
// Find nearest existing candidate
|
||||||
|
Candidate* best = nullptr;
|
||||||
|
double best_d2 = 1e9;
|
||||||
|
for (auto& c : candidates_) {
|
||||||
|
double dx = center.x() - c.center.x();
|
||||||
|
double dy = center.y() - c.center.y();
|
||||||
|
double d2 = dx*dx + dy*dy;
|
||||||
|
if (d2 < MERGE_DIST * MERGE_DIST && d2 < best_d2) {
|
||||||
|
best_d2 = d2; best = &c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best) {
|
||||||
|
// Update existing candidate
|
||||||
|
best->center = (best->center + center) / 2.0;
|
||||||
|
best->hits++;
|
||||||
|
best->age = 0;
|
||||||
|
} else {
|
||||||
|
// New candidate
|
||||||
|
candidates_.push_back({center, 1, 0});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Age all candidates, remove stale ──
|
||||||
|
for (auto& c : candidates_) c.age++;
|
||||||
|
candidates_.erase(
|
||||||
|
std::remove_if(candidates_.begin(), candidates_.end(),
|
||||||
|
[](const Candidate& c) { return c.age > 10 && c.hits < CONFIRM_HITS; }),
|
||||||
|
candidates_.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector2d> ConeUpdater::popConfirmed() {
|
||||||
|
std::vector<Eigen::Vector2d> confirmed;
|
||||||
|
for (auto it = candidates_.begin(); it != candidates_.end(); ) {
|
||||||
|
if (it->hits >= CONFIRM_HITS) {
|
||||||
|
confirmed.push_back(it->center);
|
||||||
|
it = candidates_.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace car_nav_lite
|
||||||
@@ -67,6 +67,10 @@ void GridMap::composeLayers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GridMap::decayDynamic(uint8_t amount) {
|
||||||
|
for (auto& v : dynamic_cells_) v = (v > amount) ? (v - amount) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
void GridMap::freezeSessionMap() {
|
void GridMap::freezeSessionMap() {
|
||||||
frozen_ = true;
|
frozen_ = true;
|
||||||
// Merge cone layer into static for persistence
|
// Merge cone layer into static for persistence
|
||||||
@@ -163,6 +167,7 @@ void GridMap::updateScan(const LaserScan& scan, const Pose2D& pose) {
|
|||||||
int ox, oy;
|
int ox, oy;
|
||||||
if (!worldToGrid(pose.x, pose.y, ox, oy)) return;
|
if (!worldToGrid(pose.x, pose.y, ox, oy)) return;
|
||||||
|
|
||||||
|
if (frozen_) decayDynamic(10);
|
||||||
auto& target = frozen_ ? dynamic_cells_ : cells_;
|
auto& target = frozen_ ? dynamic_cells_ : cells_;
|
||||||
for (int i = 0; i < scan.size(); ++i) {
|
for (int i = 0; i < scan.size(); ++i) {
|
||||||
double r = scan.ranges[i];
|
double r = scan.ranges[i];
|
||||||
@@ -239,6 +244,32 @@ void GridMap::markCircle(double wx, double wy, double radius, uint8_t value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GridMap::markFenceModel(const FenceModel& fence, double thickness) {
|
||||||
|
if (!fence.valid) return;
|
||||||
|
// Draw four edges using pre-computed rotated corner points
|
||||||
|
struct Seg { double x1,y1,x2,y2; };
|
||||||
|
Seg segs[] = {
|
||||||
|
{fence.p_lb_x,fence.p_lb_y, fence.p_lt_x,fence.p_lt_y}, // left
|
||||||
|
{fence.p_rb_x,fence.p_rb_y, fence.p_rt_x,fence.p_rt_y}, // right
|
||||||
|
{fence.p_lb_x,fence.p_lb_y, fence.p_rb_x,fence.p_rb_y}, // bottom
|
||||||
|
{fence.p_lt_x,fence.p_lt_y, fence.p_rt_x,fence.p_rt_y}, // top
|
||||||
|
};
|
||||||
|
int t = (int)(thickness / GRID_RES) + 1;
|
||||||
|
for (auto& s : segs) {
|
||||||
|
int gx1, gy1, gx2, gy2;
|
||||||
|
if (!worldToGrid(s.x1,s.y1,gx1,gy1) || !worldToGrid(s.x2,s.y2,gx2,gy2)) continue;
|
||||||
|
int dx=std::abs(gx2-gx1), dy=-std::abs(gy2-gy1);
|
||||||
|
int sx=gx1<gx2?1:-1, sy=gy1<gy2?1:-1;
|
||||||
|
int err=dx+dy, x=gx1, y=gy1;
|
||||||
|
while(true){for(int ty2=-t;ty2<=t;++ty2)for(int tx2=-t;tx2<=t;++tx2)
|
||||||
|
if(tx2*tx2+ty2*ty2<=t*t&&inBounds(x+tx2,y+ty2))
|
||||||
|
static_cells_[(y+ty2)*GRID_SIZE+(x+tx2)]=255;
|
||||||
|
if(x==gx2&&y==gy2)break;int e2=2*err;
|
||||||
|
if(e2>=dy){err+=dy;x+=sx;}if(e2<=dx){err+=dx;y+=sy;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GridMap::markCone(double wx, double wy, double radius, uint8_t value) {
|
void GridMap::markCone(double wx, double wy, double radius, uint8_t value) {
|
||||||
int cx, cy, cr;
|
int cx, cy, cr;
|
||||||
if (!worldToGrid(wx, wy, cx, cy)) return;
|
if (!worldToGrid(wx, wy, cx, cy)) return;
|
||||||
|
|||||||
50
src/laser_safety.cpp
Normal file
50
src/laser_safety.cpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#include "car_nav_lite/laser_safety.hpp"
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace car_nav_lite {
|
||||||
|
|
||||||
|
Twist LaserSafetyFilter::filter(const Twist& cmd, const LaserScan& scan) const {
|
||||||
|
Twist out = cmd;
|
||||||
|
if (!enable || scan.size() < 10) return out;
|
||||||
|
|
||||||
|
double front_min = 999.0;
|
||||||
|
double left_sum = 0, right_sum = 0;
|
||||||
|
int left_n = 0, right_n = 0;
|
||||||
|
double half_w = front_width;
|
||||||
|
double stop = stop_dist + std::max(0.0, cmd.vx) * 0.5;
|
||||||
|
double slow = slow_dist + std::max(0.0, cmd.vx) * 0.5;
|
||||||
|
|
||||||
|
for (int i = 0; i < scan.size(); ++i) {
|
||||||
|
double r = scan.ranges[i];
|
||||||
|
if (r < 0.10 || r > max_range) continue;
|
||||||
|
double a = scan.angle_min + i * scan.angle_increment;
|
||||||
|
double x = r * std::cos(a); // body-frame
|
||||||
|
double y = r * std::sin(a);
|
||||||
|
if (x <= 0) continue; // behind car
|
||||||
|
|
||||||
|
// Front safety zone
|
||||||
|
if (std::abs(y) < half_w && x < front_min) front_min = x;
|
||||||
|
|
||||||
|
// Side clearance for evasive turn
|
||||||
|
if (x > 0 && x < 1.0) {
|
||||||
|
if (y > 0) { left_sum += r; left_n++; }
|
||||||
|
else { right_sum += r; right_n++; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double left_avg = left_n > 0 ? left_sum / left_n : 0;
|
||||||
|
double right_avg = right_n > 0 ? right_sum / right_n : 0;
|
||||||
|
|
||||||
|
if (front_min < stop) {
|
||||||
|
// Ackermann: cannot turn in place. Stop means stop.
|
||||||
|
out.vx = 0.0;
|
||||||
|
out.wz = 0.0;
|
||||||
|
} else if (front_min < slow) {
|
||||||
|
out.vx *= 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace car_nav_lite
|
||||||
@@ -77,8 +77,20 @@ double LocalPlanner::evaluateTrajectory(
|
|||||||
double end_to_goal = std::hypot(goal.x - sx, goal.y - sy);
|
double end_to_goal = std::hypot(goal.x - sx, goal.y - sy);
|
||||||
cost += W_FOLLOW * end_to_goal;
|
cost += W_FOLLOW * end_to_goal;
|
||||||
|
|
||||||
// ── Reverse penalty (mild) ──
|
// ── Progress critic: reward forward progress along path ──
|
||||||
if (vx < 0) cost += std::abs(vx) * 3.0;
|
if (total_path_len > 0.5) {
|
||||||
|
// Find start and end path indices
|
||||||
|
size_t s0 = 0, s1 = 0;
|
||||||
|
double d0 = 1e9, d1 = 1e9;
|
||||||
|
for (size_t i = 0; i < path.size(); ++i) {
|
||||||
|
double ds = std::hypot(pose.x - path[i].x, pose.y - path[i].y);
|
||||||
|
if (ds < d0) { d0 = ds; s0 = i; }
|
||||||
|
double de = std::hypot(sx - path[i].x, sy - path[i].y);
|
||||||
|
if (de < d1) { d1 = de; s1 = i; }
|
||||||
|
}
|
||||||
|
double progress = std::max(0.0, path_dists[s1] - path_dists[s0]);
|
||||||
|
cost += 20.0 * std::max(0.0, 0.25 - progress); // penalty for too little progress
|
||||||
|
}
|
||||||
|
|
||||||
return cost;
|
return cost;
|
||||||
}
|
}
|
||||||
@@ -107,20 +119,22 @@ Twist LocalPlanner::compute(const Pose2D& pose,
|
|||||||
for (int k = 0; k < K; ++k) {
|
for (int k = 0; k < K; ++k) {
|
||||||
double vx, wz;
|
double vx, wz;
|
||||||
if (k < exploit_n) {
|
if (k < exploit_n) {
|
||||||
// Exploit: sample around previous optimal
|
// Exploit: sample v + kappa (Ackermann-compatible)
|
||||||
vx = prev_vx_ + VX_STD * randNormal();
|
vx = prev_vx_ + VX_STD * randNormal();
|
||||||
wz = prev_wz_ + WZ_STD * randNormal();
|
double kappa = (prev_vx_ > 0.01) ? (prev_wz_ / prev_vx_ + WZ_STD * randNormal()) : (WZ_STD * randNormal());
|
||||||
|
wz = vx * kappa;
|
||||||
} else {
|
} else {
|
||||||
// Explore: uniform random across full range (including reverse)
|
// Explore: uniform vx + kappa (Ackermann-compatible)
|
||||||
uint32_t x = rng_state_;
|
uint32_t x = rng_state_;
|
||||||
x ^= x << 13; x ^= x >> 17; x ^= x << 5; rng_state_ = x;
|
x ^= x << 13; x ^= x >> 17; x ^= x << 5; rng_state_ = x;
|
||||||
vx = (double)(x & 0xFFFF) / 65535.0 * (ms_ + 0.5) - 0.5;
|
vx = (double)(x & 0xFFFF) / 65535.0 * ms_;
|
||||||
x ^= x << 13; x ^= x >> 17; x ^= x << 5; rng_state_ = x;
|
x ^= x << 13; x ^= x >> 17; x ^= x << 5; rng_state_ = x;
|
||||||
wz = ((double)(x & 0xFFFF) / 65535.0 - 0.5) * 3.0;
|
double kappa = ((double)(x & 0xFFFF) / 65535.0 - 0.5) * (2.0 / mr_);
|
||||||
|
wz = vx * kappa;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp + Ackermann (no hard wz limit — Ackermann constraint is enough)
|
// Clamp + Ackermann: no reverse, curvature-based wz
|
||||||
vx = limit(vx, -0.5, ms_);
|
vx = limit(vx, 0.0, ms_);
|
||||||
double max_wz = std::abs(vx) / mr_;
|
double max_wz = std::abs(vx) / mr_;
|
||||||
wz = limit(wz, -max_wz, max_wz);
|
wz = limit(wz, -max_wz, max_wz);
|
||||||
|
|
||||||
@@ -152,9 +166,6 @@ Twist LocalPlanner::compute(const Pose2D& pose,
|
|||||||
cmd.vx = weighted_vx / sum_weights;
|
cmd.vx = weighted_vx / sum_weights;
|
||||||
cmd.wz = weighted_wz / sum_weights;
|
cmd.wz = weighted_wz / sum_weights;
|
||||||
|
|
||||||
// Enforce minimum reverse speed
|
|
||||||
if (cmd.vx < 0 && cmd.vx > -0.3) cmd.vx = -0.3;
|
|
||||||
|
|
||||||
cmd.wz = limit(cmd.wz, -std::abs(cmd.vx) / mr_, std::abs(cmd.vx) / mr_);
|
cmd.wz = limit(cmd.wz, -std::abs(cmd.vx) / mr_, std::abs(cmd.vx) / mr_);
|
||||||
|
|
||||||
prev_vx_ = cmd.vx;
|
prev_vx_ = cmd.vx;
|
||||||
|
|||||||
@@ -40,7 +40,51 @@ void Localizer::correctWithScanCV(const LaserScan& scan, const GridMap& map,
|
|||||||
const int N = scan.size();
|
const int N = scan.size();
|
||||||
Pose2D base = base_override ? *base_override : odom_pose_;
|
Pose2D base = base_override ? *base_override : odom_pose_;
|
||||||
|
|
||||||
// ── 1. Render scan to local image ──
|
// ── 1. Point-to-Line2D residual (primary, no Hough needed) ──
|
||||||
|
if (use_fence_ && fence_.valid) {
|
||||||
|
constexpr double GATE = 0.20;
|
||||||
|
double H[9]={0}, b2[3]={0}; double sum_r2=0; int n_pts=0;
|
||||||
|
for (int i=0;i<N;i+=2){double r=scan.ranges[i];if(r<0.1||r>4.5)continue;
|
||||||
|
double a=scan.angle_min+i*scan.angle_increment;
|
||||||
|
double wx=base.x+r*cos(a+base.yaw),wy=base.y+r*sin(a+base.yaw);
|
||||||
|
const Line2D*best=nullptr;double best_d=1e9;
|
||||||
|
for(auto*L:{&fence_.left,&fence_.right,&fence_.bottom,&fence_.top}){
|
||||||
|
double d=std::abs(L->nx*wx+L->ny*wy-L->d);if(d<best_d){best_d=d;best=L;}}
|
||||||
|
if(best_d>GATE||!best)continue;
|
||||||
|
double res=best->nx*wx+best->ny*wy-best->d;sum_r2+=res*res;
|
||||||
|
double qx=r*cos(a),qy=r*sin(a);
|
||||||
|
double dRx=-sin(base.yaw)*qx-cos(base.yaw)*qy,dRy=cos(base.yaw)*qx-sin(base.yaw)*qy;
|
||||||
|
double Jyaw=best->nx*dRx+best->ny*dRy;
|
||||||
|
double w=(std::abs(res)<0.05)?1.0:0.05/std::abs(res);
|
||||||
|
double J[3]={best->nx,best->ny,Jyaw};
|
||||||
|
for(int ri=0;ri<3;++ri){b2[ri]-=w*J[ri]*res;for(int ci=0;ci<3;++ci)H[ri*3+ci]+=w*J[ri]*J[ci];}
|
||||||
|
++n_pts;
|
||||||
|
}
|
||||||
|
double loc_rms = (n_pts>0) ? std::sqrt(sum_r2/n_pts) : 999.0;
|
||||||
|
double conf_pts = std::min(1.0, n_pts/80.0);
|
||||||
|
double conf_rms = std::min(1.0, 0.08/std::max(loc_rms,1e-3));
|
||||||
|
double quality = conf_pts * conf_rms;
|
||||||
|
loc_rms_=loc_rms; loc_conf_=quality*0.9; loc_lines_=n_pts/10;
|
||||||
|
|
||||||
|
// Only apply correction if quality is adequate
|
||||||
|
double det=H[0]*(H[4]*H[8]-H[5]*H[7])-H[1]*(H[3]*H[8]-H[5]*H[6])+H[2]*(H[3]*H[7]-H[4]*H[6]);
|
||||||
|
if (n_pts>20 && quality>0.25 && std::abs(det)>1e-12) {
|
||||||
|
double inv=1.0/det;
|
||||||
|
double dx=(H[4]*H[8]-H[5]*H[7])*b2[0]+(H[2]*H[7]-H[1]*H[8])*b2[1]+(H[1]*H[5]-H[2]*H[4])*b2[2];
|
||||||
|
double dy=(H[5]*H[6]-H[3]*H[8])*b2[0]+(H[0]*H[8]-H[2]*H[6])*b2[1]+(H[2]*H[3]-H[0]*H[5])*b2[2];
|
||||||
|
double da=(H[3]*H[7]-H[4]*H[6])*b2[0]+(H[1]*H[6]-H[0]*H[7])*b2[1]+(H[0]*H[4]-H[1]*H[3])*b2[2];
|
||||||
|
dx=limit(dx*inv,-0.10,0.10);dy=limit(dy*inv,-0.10,0.10);da=limit(da*inv,-0.10,0.10);
|
||||||
|
correction_.x+=0.2*dx;correction_.y+=0.2*dy;correction_.yaw+=0.2*da;
|
||||||
|
correction_.x=limit(correction_.x,-0.30,0.30);correction_.y=limit(correction_.y,-0.30,0.30);
|
||||||
|
correction_.yaw=limit(correction_.yaw,-0.25,0.25);
|
||||||
|
loc_dx_=correction_.x;loc_dy_=correction_.y;loc_dyaw_=correction_.yaw;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Quality insufficient — don't update correction, fall through to Hough
|
||||||
|
if (use_fence_) return; // fence exists: trust quality gate, skip Hough entirely
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Hough fallback: render scan to image + line detection ──
|
||||||
constexpr int IMG_SZ = 600, HALF = IMG_SZ / 2;
|
constexpr int IMG_SZ = 600, HALF = IMG_SZ / 2;
|
||||||
int cx_g, cy_g;
|
int cx_g, cy_g;
|
||||||
map.worldToGrid(base.x, base.y, cx_g, cy_g);
|
map.worldToGrid(base.x, base.y, cx_g, cy_g);
|
||||||
@@ -130,8 +174,7 @@ void Localizer::correctWithScanCV(const LaserScan& scan, const GridMap& map,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 5. Update offset ──
|
// ── 5. Hough update ──
|
||||||
// Store metrics + confidence
|
|
||||||
loc_lines_ = line_count;
|
loc_lines_ = line_count;
|
||||||
loc_conf_ = (line_count >= 3) ? 0.9 : (line_count >= 2 ? 0.6 : 0.3);
|
loc_conf_ = (line_count >= 3) ? 0.9 : (line_count >= 2 ? 0.6 : 0.3);
|
||||||
if (std::abs(dyaw) > 0.15) loc_conf_ *= 0.5;
|
if (std::abs(dyaw) > 0.15) loc_conf_ *= 0.5;
|
||||||
|
|||||||
58
src/main.cpp
58
src/main.cpp
@@ -21,6 +21,8 @@
|
|||||||
#include "car_nav_lite/bt_executor.hpp"
|
#include "car_nav_lite/bt_executor.hpp"
|
||||||
#include "car_nav_lite/fence_model.hpp"
|
#include "car_nav_lite/fence_model.hpp"
|
||||||
#include "car_nav_lite/session_mapper.hpp"
|
#include "car_nav_lite/session_mapper.hpp"
|
||||||
|
#include "car_nav_lite/cone_updater.hpp"
|
||||||
|
#include "car_nav_lite/laser_safety.hpp"
|
||||||
|
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
using namespace car_nav_lite;
|
using namespace car_nav_lite;
|
||||||
@@ -46,6 +48,7 @@ public:
|
|||||||
declare_parameter("wall_ransac_thresh", 0.04);
|
declare_parameter("wall_ransac_thresh", 0.04);
|
||||||
declare_parameter("cone_mark_radius", 0.18);
|
declare_parameter("cone_mark_radius", 0.18);
|
||||||
declare_parameter("dynamic_obstacle_enable", false);
|
declare_parameter("dynamic_obstacle_enable", false);
|
||||||
|
declare_parameter("online_cone_enable", true);
|
||||||
_read_params();
|
_read_params();
|
||||||
|
|
||||||
param_cb_ = add_on_set_parameters_callback(
|
param_cb_ = add_on_set_parameters_callback(
|
||||||
@@ -58,6 +61,13 @@ public:
|
|||||||
else if (n=="steer_limit") sl_=p.as_double();
|
else if (n=="steer_limit") sl_=p.as_double();
|
||||||
else if (n=="speed_curv_factor") cf_=p.as_double();
|
else if (n=="speed_curv_factor") cf_=p.as_double();
|
||||||
else if (n=="test_mode") { test_=p.as_bool(); tps_=false; }
|
else if (n=="test_mode") { test_=p.as_bool(); tps_=false; }
|
||||||
|
else if (n=="calib_scan_count") calib_scan_count_=p.as_int();
|
||||||
|
else if (n=="fence_expected_size") fence_expected_size_=p.as_double();
|
||||||
|
else if (n=="fence_size_tolerance") fence_size_tolerance_=p.as_double();
|
||||||
|
else if (n=="wall_ransac_thresh") wall_ransac_thresh_=p.as_double();
|
||||||
|
else if (n=="cone_mark_radius") cone_mark_radius_=p.as_double();
|
||||||
|
else if (n=="dynamic_obstacle_enable") dynamic_obstacle_enable_=p.as_bool();
|
||||||
|
else if (n=="online_cone_enable") online_cone_enable_=p.as_bool();
|
||||||
}
|
}
|
||||||
RCLCPP_INFO(get_logger(),"params: la=%.2f ms=%.1f mr=%.2f sl=%.2f cf=%.1f test=%d",
|
RCLCPP_INFO(get_logger(),"params: la=%.2f ms=%.1f mr=%.2f sl=%.2f cf=%.1f test=%d",
|
||||||
la_,ms_,mr_,sl_,cf_,test_);
|
la_,ms_,mr_,sl_,cf_,test_);
|
||||||
@@ -110,7 +120,7 @@ private:
|
|||||||
int calib_scan_count_=30;
|
int calib_scan_count_=30;
|
||||||
double fence_expected_size_=5.0, fence_size_tolerance_=0.35;
|
double fence_expected_size_=5.0, fence_size_tolerance_=0.35;
|
||||||
double wall_ransac_thresh_=0.04, cone_mark_radius_=0.18;
|
double wall_ransac_thresh_=0.04, cone_mark_radius_=0.18;
|
||||||
bool dynamic_obstacle_enable_=false;
|
bool dynamic_obstacle_enable_=false, online_cone_enable_=true;
|
||||||
rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr param_cb_;
|
rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr param_cb_;
|
||||||
void _read_params(){
|
void _read_params(){
|
||||||
mf_=get_parameter("map_file").as_string();
|
mf_=get_parameter("map_file").as_string();
|
||||||
@@ -126,6 +136,7 @@ private:
|
|||||||
wall_ransac_thresh_=get_parameter("wall_ransac_thresh").as_double();
|
wall_ransac_thresh_=get_parameter("wall_ransac_thresh").as_double();
|
||||||
cone_mark_radius_=get_parameter("cone_mark_radius").as_double();
|
cone_mark_radius_=get_parameter("cone_mark_radius").as_double();
|
||||||
dynamic_obstacle_enable_=get_parameter("dynamic_obstacle_enable").as_bool();
|
dynamic_obstacle_enable_=get_parameter("dynamic_obstacle_enable").as_bool();
|
||||||
|
online_cone_enable_=get_parameter("online_cone_enable").as_bool();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── State ──
|
// ── State ──
|
||||||
@@ -138,6 +149,10 @@ private:
|
|||||||
GridMap map_; Localizer localizer_; BTExecutor bt_;
|
GridMap map_; Localizer localizer_; BTExecutor bt_;
|
||||||
SessionMapper session_mapper_;
|
SessionMapper session_mapper_;
|
||||||
FenceModel fence_model_;
|
FenceModel fence_model_;
|
||||||
|
ConeUpdater cone_updater_;
|
||||||
|
LaserSafetyFilter safety_filter_;
|
||||||
|
std::vector<Eigen::Vector2d> known_cones_;
|
||||||
|
rclcpp::Time last_cone_tick_{0,0,RCL_ROS_TIME};
|
||||||
enum SessionState { CALIBRATING, READY };
|
enum SessionState { CALIBRATING, READY };
|
||||||
SessionState session_state_ = CALIBRATING;
|
SessionState session_state_ = CALIBRATING;
|
||||||
int calib_cnt_ = 0;
|
int calib_cnt_ = 0;
|
||||||
@@ -166,14 +181,16 @@ private:
|
|||||||
std::vector<Eigen::Vector2d> cones;
|
std::vector<Eigen::Vector2d> cones;
|
||||||
if (session_mapper_.build(fence_model_, cones, fence_expected_size_, fence_size_tolerance_, wall_ransac_thresh_)) {
|
if (session_mapper_.build(fence_model_, cones, fence_expected_size_, fence_size_tolerance_, wall_ransac_thresh_)) {
|
||||||
localizer_.setFenceModel(fence_model_);
|
localizer_.setFenceModel(fence_model_);
|
||||||
|
known_cones_ = cones;
|
||||||
for (auto& c : cones) map_.markCone(c.x(), c.y(), cone_mark_radius_, 255);
|
for (auto& c : cones) map_.markCone(c.x(), c.y(), cone_mark_radius_, 255);
|
||||||
|
map_.markFenceModel(fence_model_);
|
||||||
map_.morphologyClose(3);
|
map_.morphologyClose(3);
|
||||||
map_.freezeSessionMap();
|
map_.freezeSessionMap();
|
||||||
session_state_ = READY;
|
session_state_ = READY;
|
||||||
fprintf(stderr, "[Session] READY — fence %.2fx%.2f yaw=%.1f°, %zu cones\n",
|
fprintf(stderr, "[Session] READY — fence %.2fx%.2f yaw=%.1f°, %zu cones\n",
|
||||||
fence_model_.width, fence_model_.height, fence_model_.yaw_field*57.3, cones.size());
|
fence_model_.width, fence_model_.height, fence_model_.yaw_field*57.3, cones.size());
|
||||||
} else {
|
} else {
|
||||||
calib_cnt_ = 0;
|
calib_cnt_ = 0; session_mapper_.reset();
|
||||||
fprintf(stderr, "[Session] calibration failed, retrying...\n");
|
fprintf(stderr, "[Session] calibration failed, retrying...\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,7 +213,7 @@ private:
|
|||||||
last_imu_t_=now;latest_gyro_z_=m->angular_velocity.z;
|
last_imu_t_=now;latest_gyro_z_=m->angular_velocity.z;
|
||||||
if(dt>0&&dt<1.0)localizer_.updateIMU(latest_gyro_z_,dt);}
|
if(dt>0&&dt<1.0)localizer_.updateIMU(latest_gyro_z_,dt);}
|
||||||
void goal_cb(PoseStMsg::SharedPtr m){std::lock_guard lk(mtx_);
|
void goal_cb(PoseStMsg::SharedPtr m){std::lock_guard lk(mtx_);
|
||||||
if(m->header.frame_id!="map"){RCLCPP_WARN(get_logger(),"goal frame not map");return;}
|
if(m->header.frame_id!="map"){RCLCPP_WARN(get_logger(),"goal frame not map: '%s'",m->header.frame_id.c_str());return;}
|
||||||
Pose2D g(m->pose.position.x,m->pose.position.y,0.0);
|
Pose2D g(m->pose.position.x,m->pose.position.y,0.0);
|
||||||
double qw=m->pose.orientation.w,qz=m->pose.orientation.z;
|
double qw=m->pose.orientation.w,qz=m->pose.orientation.z;
|
||||||
if(qw!=0||qz!=0)g.yaw=std::atan2(2.0*qw*qz,qw*qw-qz*qz);
|
if(qw!=0||qz!=0)g.yaw=std::atan2(2.0*qw*qz,qw*qw-qz*qz);
|
||||||
@@ -242,9 +259,29 @@ private:
|
|||||||
void tick(){
|
void tick(){
|
||||||
std::lock_guard lk(mtx_);
|
std::lock_guard lk(mtx_);
|
||||||
auto pose=localizer_.pose();publish_tf(pose);
|
auto pose=localizer_.pose();publish_tf(pose);
|
||||||
// READY: only update scan if dynamic obstacles enabled
|
// CALIBRATING: force zero cmd, wait for fence model
|
||||||
if(has_scan_ && (session_state_==CALIBRATING || dynamic_obstacle_enable_))
|
if (session_state_ == CALIBRATING) {
|
||||||
map_.updateScan(latest_scan_,pose);
|
pub_cmd_->publish(geometry_msgs::msg::Twist());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// READY: online cone discovery (3Hz) + optional dynamic obstacles
|
||||||
|
if (has_scan_) {
|
||||||
|
auto cone_now = this->now();
|
||||||
|
if (online_cone_enable_ && (cone_now - last_cone_tick_).seconds() > 0.3) {
|
||||||
|
last_cone_tick_ = cone_now;
|
||||||
|
cone_updater_.update(latest_scan_, pose, fence_model_, known_cones_);
|
||||||
|
auto new_cones = cone_updater_.popConfirmed();
|
||||||
|
for (auto& c : new_cones) {
|
||||||
|
known_cones_.push_back(c);
|
||||||
|
map_.markCone(c.x(), c.y(), cone_mark_radius_, 255);
|
||||||
|
fprintf(stderr, "[Session] new cone: (%.2f,%.2f) total=%zu\n",
|
||||||
|
c.x(), c.y(), known_cones_.size());
|
||||||
|
}
|
||||||
|
if (!new_cones.empty()) map_.composeLayers();
|
||||||
|
}
|
||||||
|
if (dynamic_obstacle_enable_)
|
||||||
|
map_.updateScan(latest_scan_, pose);
|
||||||
|
}
|
||||||
|
|
||||||
// Test mode: auto-activate figure-8 after 3s
|
// Test mode: auto-activate figure-8 after 3s
|
||||||
if(test_&&!tps_){
|
if(test_&&!tps_){
|
||||||
@@ -263,6 +300,11 @@ private:
|
|||||||
// Slow down if localization confidence is low
|
// Slow down if localization confidence is low
|
||||||
if (localizer_.loc_conf_ < 0.3) { cmd.vx *= 0.4; cmd.wz *= 0.4; }
|
if (localizer_.loc_conf_ < 0.3) { cmd.vx *= 0.4; cmd.wz *= 0.4; }
|
||||||
|
|
||||||
|
// Laser safety filter: last-moment scan check
|
||||||
|
if (has_scan_ && session_state_ == READY) {
|
||||||
|
Twist safe = safety_filter_.filter({(float)cmd.vx, (float)cmd.wz}, latest_scan_);
|
||||||
|
cmd.vx = safe.vx; cmd.wz = safe.wz;
|
||||||
|
}
|
||||||
auto tw=geometry_msgs::msg::Twist();tw.linear.x=cmd.vx;tw.angular.z=cmd.wz;
|
auto tw=geometry_msgs::msg::Twist();tw.linear.x=cmd.vx;tw.angular.z=cmd.wz;
|
||||||
pub_cmd_->publish(tw);
|
pub_cmd_->publish(tw);
|
||||||
if(!bt_.currentPath().empty()){
|
if(!bt_.currentPath().empty()){
|
||||||
@@ -301,10 +343,10 @@ private:
|
|||||||
char buf[512];
|
char buf[512];
|
||||||
snprintf(buf, sizeof(buf),
|
snprintf(buf, sizeof(buf),
|
||||||
"{\"state\":\"%s\",\"x\":%.2f,\"y\":%.2f,\"yaw\":%.1f,"
|
"{\"state\":\"%s\",\"x\":%.2f,\"y\":%.2f,\"yaw\":%.1f,"
|
||||||
"\"loc_lines\":%d,\"loc_dx\":%.3f,\"loc_dy\":%.3f,\"loc_dyaw\":%.2f,"
|
"\"loc_conf\":%.2f,\"loc_rms\":%.3f,\"loc_lines\":%d,\"loc_dx\":%.3f,\"loc_dy\":%.3f,\"loc_dyaw\":%.2f,"
|
||||||
"\"mppi_alive\":%d,\"mppi_total\":%d,\"mppi_minC\":%.1f}",
|
"\"mppi_alive\":%d,\"mppi_total\":%d,\"mppi_minC\":%.1f}",
|
||||||
m.state, m.pose_x, m.pose_y, m.pose_yaw*57.3,
|
m.state, m.pose_x, m.pose_y, m.pose_yaw*57.3,
|
||||||
m.loc_lines, m.loc_dx, m.loc_dy, m.loc_dyaw*57.3,
|
localizer_.loc_conf_, localizer_.loc_rms_, m.loc_lines, m.loc_dx, m.loc_dy, m.loc_dyaw*57.3,
|
||||||
m.mppi_alive, m.mppi_total, m.mppi_min_cost);
|
m.mppi_alive, m.mppi_total, m.mppi_min_cost);
|
||||||
std_msgs::msg::String msg; msg.data = buf;
|
std_msgs::msg::String msg; msg.data = buf;
|
||||||
pub_metrics_->publish(msg);
|
pub_metrics_->publish(msg);
|
||||||
|
|||||||
@@ -89,12 +89,16 @@ bool SessionMapper::build(FenceModel& fence, std::vector<Eigen::Vector2d>& cones
|
|||||||
double y_min = ys[lo_idx], y_max = ys[hi_idx];
|
double y_min = ys[lo_idx], y_max = ys[hi_idx];
|
||||||
double w = x_max - x_min, h = y_max - y_min;
|
double w = x_max - x_min, h = y_max - y_min;
|
||||||
|
|
||||||
// Validate size
|
// Validate size — allow known-size fallback if partial fence
|
||||||
if (std::abs(w - expected_size) > size_tol ||
|
if (std::abs(w - expected_size) > size_tol ||
|
||||||
std::abs(h - expected_size) > size_tol) {
|
std::abs(h - expected_size) > size_tol) {
|
||||||
fprintf(stderr, "[SessionMapper] size mismatch: %.2fx%.2f (expected %.1f±%.1f)\n",
|
fprintf(stderr, "[SessionMapper] partial fence %.2fx%.2f, fallback to known %.1fx%.1f\n",
|
||||||
w, h, expected_size, size_tol);
|
w, h, expected_size, expected_size);
|
||||||
return false;
|
// Use known field size centered at origin with estimated yaw
|
||||||
|
double half = expected_size / 2.0;
|
||||||
|
x_min = -half; x_max = half;
|
||||||
|
y_min = -half; y_max = half;
|
||||||
|
w = h = expected_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
fence.width = w; fence.height = h;
|
fence.width = w; fence.height = h;
|
||||||
@@ -115,23 +119,23 @@ bool SessionMapper::build(FenceModel& fence, std::vector<Eigen::Vector2d>& cones
|
|||||||
Eigen::Vector2d tw = rotBack(fence.cx, y_max);
|
Eigen::Vector2d tw = rotBack(fence.cx, y_max);
|
||||||
fence.left_x = lw.x(); fence.right_x = rw.x();
|
fence.left_x = lw.x(); fence.right_x = rw.x();
|
||||||
fence.bottom_y = bw.y(); fence.top_y = tw.y();
|
fence.bottom_y = bw.y(); fence.top_y = tw.y();
|
||||||
|
// Corner points for drawing rotated fence (lb/lt/rb/rt are already computed above)
|
||||||
|
|
||||||
// Left wall: field-x = x_min, direction = field-y (0, 1)
|
// Build fence lines from field-aligned extents (NOT from 0/w/h!)
|
||||||
Eigen::Vector2d lb = rotBack(x_min, 0), lt = rotBack(x_min, h);
|
Eigen::Vector2d lb = rotBack(x_min, y_min), lt = rotBack(x_min, y_max);
|
||||||
fence.left.setFromTwoPoints(lb.x(), lb.y(), lt.x(), lt.y());
|
fence.left.setFromTwoPoints(lb.x(), lb.y(), lt.x(), lt.y());
|
||||||
|
Eigen::Vector2d rb = rotBack(x_max, y_min), rt = rotBack(x_max, y_max);
|
||||||
// Right wall
|
|
||||||
Eigen::Vector2d rb = rotBack(x_max, 0), rt = rotBack(x_max, h);
|
|
||||||
fence.right.setFromTwoPoints(rb.x(), rb.y(), rt.x(), rt.y());
|
fence.right.setFromTwoPoints(rb.x(), rb.y(), rt.x(), rt.y());
|
||||||
|
Eigen::Vector2d bb = rotBack(x_min, y_min), br = rotBack(x_max, y_min);
|
||||||
// Bottom wall: field-y = y_min
|
|
||||||
Eigen::Vector2d bb = rotBack(0, y_min), br = rotBack(w, y_min);
|
|
||||||
fence.bottom.setFromTwoPoints(bb.x(), bb.y(), br.x(), br.y());
|
fence.bottom.setFromTwoPoints(bb.x(), bb.y(), br.x(), br.y());
|
||||||
|
Eigen::Vector2d tb = rotBack(x_min, y_max), tr_ = rotBack(x_max, y_max);
|
||||||
// Top wall
|
|
||||||
Eigen::Vector2d tb = rotBack(0, y_max), tr_ = rotBack(w, y_max);
|
|
||||||
fence.top.setFromTwoPoints(tb.x(), tb.y(), tr_.x(), tr_.y());
|
fence.top.setFromTwoPoints(tb.x(), tb.y(), tr_.x(), tr_.y());
|
||||||
|
|
||||||
|
fence.p_lb_x=lb.x();fence.p_lb_y=lb.y();
|
||||||
|
fence.p_lt_x=lt.x();fence.p_lt_y=lt.y();
|
||||||
|
fence.p_rb_x=rb.x();fence.p_rb_y=rb.y();
|
||||||
|
fence.p_rt_x=rt.x();fence.p_rt_y=rt.y();
|
||||||
|
|
||||||
fence.valid = true;
|
fence.valid = true;
|
||||||
|
|
||||||
// ── 5. Extract cones: non-wall points → cluster ──
|
// ── 5. Extract cones: non-wall points → cluster ──
|
||||||
|
|||||||
Reference in New Issue
Block a user