Skip to content

Commit 0d0574c

Browse files
committed
feat(bt): TDD goal extraction BT node development. Frontier detection, component generation working and tested. Visual artifacts produced during testing, manually validated against costmap extracted from simulation.
1 parent 7eed13a commit 0d0574c

15 files changed

Lines changed: 1028 additions & 88 deletions

File tree

.vscode/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
"C_Cpp.clang_format_style": "file",
77
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
88
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/omniseer_description/compile_commands.json",
9+
"C_Cpp.default.includePath": [
10+
"${workspaceFolder}/include", // so omniseer/... works
11+
"${workspaceFolder}/**"
12+
],
913
"cmake.ignoreCMakeListsMissing": true,
1014
"files.associations": {
1115
"cctype": "cpp",
@@ -88,6 +92,7 @@
8892
"ranges": "cpp",
8993
"shared_mutex": "cpp",
9094
"stdfloat": "cpp",
91-
"valarray": "cpp"
95+
"valarray": "cpp",
96+
"filesystem": "cpp"
9297
}
9398
}

ros_ws/src/omniseer_description/CMakeLists.txt

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ endif()
88
find_package(ament_cmake REQUIRED)
99
find_package(xacro REQUIRED)
1010
find_package(geometry_msgs REQUIRED)
11-
find_package(ament_cmake REQUIRED)
1211
find_package(pluginlib REQUIRED)
1312
find_package(behaviortree_cpp REQUIRED)
1413
find_package(rclcpp REQUIRED)
@@ -20,12 +19,24 @@ find_package(tf2 REQUIRED)
2019
find_package(tf2_ros REQUIRED)
2120
find_package(tf2_geometry_msgs REQUIRED)
2221

23-
add_library(omniseer_bt_nodes SHARED
24-
src/select_frontier_from_costmap.cpp
22+
# --- Core (ROS-agnostic) ---
23+
add_library(omniseer_frontier_core STATIC
2524
src/frontier.cpp
2625
src/grid_io.cpp
2726
)
2827

28+
target_include_directories(omniseer_frontier_core PUBLIC
29+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
30+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/third_party>
31+
$<INSTALL_INTERFACE:include>)
32+
33+
target_compile_features(omniseer_frontier_core PUBLIC cxx_std_17)
34+
35+
# --- ROS BT plugin ---
36+
add_library(omniseer_bt_nodes SHARED
37+
src/select_frontier_from_costmap.cpp
38+
)
39+
2940
target_include_directories(omniseer_bt_nodes PUBLIC
3041
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
3142
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/third_party>
@@ -46,40 +57,54 @@ target_link_libraries(omniseer_bt_nodes
4657
tf2_geometry_msgs::tf2_geometry_msgs
4758
tf2_ros::tf2_ros)
4859

60+
target_link_libraries(omniseer_bt_nodes
61+
PRIVATE omniseer_frontier_core
62+
)
63+
4964
pluginlib_export_plugin_description_file(behaviortree_cpp plugin_description.xml)
5065

66+
# --- Install ---
5167
install(DIRECTORY urdf meshes rviz behavior_trees
5268
DESTINATION share/${PROJECT_NAME}
5369
OPTIONAL)
5470

5571
install(DIRECTORY include/ DESTINATION include)
5672

57-
install(TARGETS omniseer_bt_nodes
73+
install(TARGETS omniseer_bt_nodes omniseer_frontier_core
5874
ARCHIVE DESTINATION lib
5975
LIBRARY DESTINATION lib
6076
RUNTIME DESTINATION bin)
6177

6278
if(BUILD_TESTING)
63-
# grid io test
6479
find_package(ament_cmake_gtest REQUIRED)
80+
81+
set(OMNISEER_ARTIFACTS_DIR "${CMAKE_CURRENT_BINARY_DIR}/artifacts")
82+
set(TEST_INC "${CMAKE_CURRENT_SOURCE_DIR}/test/include")
83+
6584
ament_add_gtest(test_grid_io test/test_grid_io.cpp)
66-
target_link_libraries(test_grid_io omniseer_bt_nodes)
67-
target_compile_definitions(test_grid_io PRIVATE TEST_DIR="${CMAKE_SOURCE_DIR}")
68-
69-
# # xacro regeneration test
70-
# ament_add_test(check_xacro
71-
# COMMAND bash -c "
72-
# xacro ${CMAKE_SOURCE_DIR}/urdf/xacro/exported_robot.urdf.xacro \
73-
# > ${CMAKE_BINARY_DIR}/rebuilt.urdf &&
74-
# diff -u --ignore-all-space ${CMAKE_SOURCE_DIR}/urdf/exported_robot.urdf \
75-
# ${CMAKE_BINARY_DIR}/rebuilt.urdf
76-
# "
77-
# WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
78-
79-
# find_package(ament_lint_auto REQUIRED)
80-
# set(ament_cmake_copyright_FOUND TRUE)
81-
# set(ament_cmake_cpplint_FOUND TRUE)
82-
# ament_lint_auto_find_test_dependencies()
85+
target_link_libraries(test_grid_io omniseer_frontier_core)
86+
target_include_directories(test_grid_io PRIVATE ${TEST_INC})
87+
target_compile_definitions(test_grid_io PRIVATE
88+
TEST_DIR="${CMAKE_SOURCE_DIR}"
89+
)
90+
91+
ament_add_gtest(test_frontier_mask test/test_frontier_mask.cpp)
92+
target_link_libraries(test_frontier_mask omniseer_frontier_core)
93+
target_include_directories(test_frontier_mask PRIVATE ${TEST_INC})
94+
target_compile_definitions(test_frontier_mask PRIVATE
95+
TEST_DIR="${CMAKE_SOURCE_DIR}"
96+
OMNISEER_ARTIFACTS_DIR="${OMNISEER_ARTIFACTS_DIR}"
97+
)
98+
99+
ament_add_gtest(test_components test/test_components.cpp)
100+
target_link_libraries(test_components omniseer_frontier_core)
101+
target_include_directories(test_components PRIVATE ${TEST_INC})
102+
target_compile_definitions(test_components PRIVATE
103+
TEST_DIR="${CMAKE_SOURCE_DIR}"
104+
OMNISEER_ARTIFACTS_DIR="${OMNISEER_ARTIFACTS_DIR}"
105+
)
106+
107+
# more here
83108
endif()
84109

85110
ament_package()
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,135 @@
1+
#pragma once
2+
13
#include <cstdint>
24
#include <functional>
5+
#include <span>
6+
#include <string>
7+
#include <utility>
38
#include <vector>
9+
10+
#include "omniseer/grid_io.hpp"
11+
12+
/*
13+
ROS-agnostic frontier detection and goal selection API test code.
14+
15+
Purpose:
16+
- Given a costmap (GridU8 from grid_io.hpp), identify frontier cells,
17+
group them into connected components, propose feasible goal poses, and rank those
18+
goals for exploration based on information gain and distance from current position. Return maximal
19+
value goal, or -1 if none exist.
20+
21+
Types:
22+
- Pose2D: {x, y} in map frame
23+
- Conn: 4- or 8-neighborhood for component labeling.
24+
- Params: thresholds, connectivity, size filters, robot radius/clearance, range caps,
25+
and scoring weights.
26+
*/
27+
28+
namespace omniseer
29+
{
30+
31+
struct Pose2D
32+
{
33+
double x{0.0};
34+
double y{0.0};
35+
};
36+
37+
enum class Conn : uint8_t
38+
{
39+
Four = 4,
40+
Eight = 8
41+
};
42+
43+
struct Params
44+
{
45+
// --- Costmap thresholds ---
46+
uint8_t free_cost_max = 150; // 0 ..
47+
uint8_t unknown_cost = 255; // unkown
48+
uint8_t lethal_cost = 254; // lethal obstacle
49+
uint8_t inscribed_cost = 253; // inscribed obstacle
50+
51+
// Frontier = cell with at least this many UNKNOWN neighbors.
52+
Conn connectivity = Conn::Eight;
53+
54+
int min_component_size = 10; // cells; discard smaller components
55+
int min_unknown_neighbors = 1; // 1..8 depending on connectivity
56+
57+
// --- Goal generation / validation ---
58+
double robot_radius_m = 0.20;
59+
double goal_clearance_m = 0.10;
60+
double max_goal_distance_m = 20.0;
61+
62+
double w_information = 1.0;
63+
double w_distance_cost = 1.0;
64+
};
65+
66+
struct Callbacks
67+
{
68+
// Return true if goal is achievable from current position
69+
std::function<bool(const Pose2D& goal, double robot_radius_m, double clearance_m)>
70+
is_pose_navigable;
71+
72+
// Planning/path cost from robot to goal
73+
std::function<double(const Pose2D& robot, const Pose2D& goal)> plan_cost;
74+
75+
// Information gain estimate from robot to goal
76+
std::function<double(const Pose2D& goal)> information_gain;
77+
78+
// Logging hook
79+
std::function<void(const std::string& msg)> log;
80+
};
81+
82+
struct FrontierComponent
83+
{
84+
int id{-1}; // 0..K-1
85+
int size_cells{0}; // number of frontier cells
86+
std::vector<int> rim_indices; // representative frontier cell indices (y*w + x)
87+
double cx_m{0.0}, cy_m{0.0}; // centroid
88+
};
89+
90+
struct FrontierGoal
91+
{
92+
Pose2D pose;
93+
double score{0.0};
94+
int component_id{-1};
95+
};
96+
97+
struct Artifacts
98+
{
99+
int width{0}, height{0}; // dimensions for per-cell arrays
100+
std::vector<std::uint8_t> frontier_mask; // 0/1
101+
std::vector<std::int32_t> component_labels; // -1 or component id
102+
std::vector<float> distance_field_m; // >=0 or +inf
103+
std::vector<float> goal_heatmap; // arbitrary finite or NaN
104+
105+
std::vector<FrontierGoal> goals_pre_rank;
106+
std::vector<FrontierGoal> goals_ranked;
107+
};
108+
109+
// --- Pipeline API ---
110+
111+
// Compute frontier from costmap, i.e. the border between freeish and unknown
112+
// on return: out_mask size = g.width * g.height 0 = not frontier, 1 = frontier
113+
void compute_frontier_mask(const GridU8& g, const Params& p, std::uint8_t* out_mask,
114+
std::size_t n);
115+
116+
// Compute connected components on the frontier using BFS, store metadata
117+
// on entry: labels[i] = 0 for non-frontier, 1 for frontier (from compute_frontier_mask)
118+
// on return: labels[i] = -1 for non-frontier, or component id 0..K-1 for frontier
119+
std::vector<FrontierComponent> label_components(const GridU8& g, const Params& p,
120+
std::uint8_t* frontier_mask, std::size_t n);
121+
122+
// Propose feasible goals per component using callbacks for validity/cost
123+
std::vector<FrontierGoal> select_component_goals(const GridU8& g, const Params& p,
124+
const Pose2D& robot, const Callbacks& cb,
125+
const std::vector<FrontierComponent>& comps);
126+
127+
// Score and sort goals in-place (descending by score)
128+
void rank_goals(const Params& p, const Pose2D& robot, std::vector<FrontierGoal>& goals);
129+
130+
// Full pipeline with optional debug artifacts sink
131+
std::vector<FrontierGoal> find_frontier_goals(const GridU8& g, const Params& p,
132+
const Pose2D& robot, const Callbacks& cb,
133+
Artifacts* artifacts);
134+
135+
} // namespace omniseer

ros_ws/src/omniseer_description/include/omniseer/grid_io.hpp

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,25 @@
22
#include <cstdint>
33
#include <string>
44
#include <vector>
5+
/*
6+
Representation of PGM costmap (see /examples)
57
6-
struct GridU8
8+
data:
9+
- white (255) : unknown
10+
- 254/253 : obstacles
11+
- gray : inflation gradient
12+
- black (0) : free space
13+
*/
14+
namespace omniseer
715
{
8-
uint32_t width{0}, height{0};
9-
float resolution{0.f};
10-
float origin_x{0.f}, origin_y{0.f};
11-
std::vector<uint8_t> data;
12-
};
1316

14-
GridU8 load_pgm_with_meta(const std::string& pgm_path, const std::string& meta_json_path);
17+
struct GridU8
18+
{
19+
uint32_t width{0}, height{0};
20+
float resolution{0.f};
21+
float origin_x{0.f}, origin_y{0.f};
22+
std::vector<uint8_t> data;
23+
};
24+
25+
GridU8 load_pgm_with_meta(const std::string& pgm_path, const std::string& meta_json_path);
26+
} // namespace omniseer
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Visual inspection of /frontier.cpp's compute_frontier_mask output
2+
montage \
3+
-label 'Rims' components_rims.ppm \
4+
-label 'Frontier' frontier_mask.pgm \
5+
-label 'Rims Overlay' rims_overlay.ppm \
6+
-tile 2x2 -geometry +6+6 -background black -fill white -pointsize 18 \
7+
frontier_montage.png && xdg-open frontier_montage.png || echo "See frontier_montage.png"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Visual inspection of /frontier.cpp's compute_frontier_mask output
2+
montage \
3+
-label 'Free' free_mask.pgm \
4+
-label 'Unknown' unknown_mask.pgm \
5+
-label 'Frontier' frontier_mask.pgm \
6+
-label 'Overlay' frontier_overlay.ppm \
7+
-tile 2x2 -geometry +6+6 -background black -fill white -pointsize 18 \
8+
frontier_montage.png && xdg-open frontier_montage.png || echo "See frontier_montage.png"

0 commit comments

Comments
 (0)