Skip to content

Commit 2c04d71

Browse files
Keep track of balls relative to the robot (#570)
Co-authored-by: Gijs de Jong <14833076+oxkitsune@users.noreply.github.com>
1 parent e9bd385 commit 2c04d71

9 files changed

Lines changed: 38 additions & 31 deletions

File tree

deploy/config/ball_detection.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ confidence_threshold = 0.96
7474
time_budget = 1500
7575

7676
# Process noise parameter for position prediction in the Kalman filter
77-
prediction_noise = 0.0006 # ~2.45 cm per cycle or ~1.96 m per second
77+
prediction_noise = 0.0018 # ~3.5 m per second
7878

7979
# Measurement noise parameter for the Kalman filter
8080
measurement_noise = 0.01 # ~10cm per measurement

deploy/config/scan_lines.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ max_black_luminance = 60.0
1616
max_black_saturation = 160.0
1717

1818
# Green chromaticity threshold
19-
blue_chromaticity_threshold = 0.3 # not fine tuned
20-
green_chromaticity_threshold = 0.400 # tuned
21-
red_chromaticity_threshold = 0.3 # not fine tuned
19+
blue_chromaticity_threshold = 0.3 # not fine tuned
20+
green_chromaticity_threshold = 0.36 # tuned
21+
red_chromaticity_threshold = 0.3 # not fine tuned

yggdrasil/src/behavior/behaviors/walk_to_ball.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ fn walk_to_ball(
3838
mut nao_manager: ResMut<NaoManager>,
3939
ball_tracker: Res<BallTracker>,
4040
) {
41-
let Some(ball) = ball_tracker.stationary_ball() else {
41+
let Some(ball) = ball_tracker
42+
.stationary_ball()
43+
.map(|ball| pose.robot_to_world(&ball))
44+
else {
4245
return;
4346
};
4447

yggdrasil/src/behavior/engine.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use nalgebra::Point2;
99

1010
use crate::{
1111
core::config::showtime::PlayerConfig,
12-
localization::RobotPose,
1312
motion::walking_engine::Gait,
1413
sensor::{button::HeadButtons, falling::FallState, imu::IMUValues},
1514
vision::ball_detection::ball_tracker::BallTracker,
@@ -205,7 +204,6 @@ pub fn role_base(
205204
game_controller_message: Option<Res<GameControllerMessage>>,
206205
imu_values: Res<IMUValues>,
207206
ball_tracker: Res<BallTracker>,
208-
pose: Res<RobotPose>,
209207
) {
210208
commands.disable_role();
211209
let behavior = behavior_state.get();
@@ -285,7 +283,7 @@ pub fn role_base(
285283
PrimaryState::Playing { .. } => {
286284
let possible_ball_distance = ball_tracker
287285
.stationary_ball()
288-
.map(|ball| pose.distance_to(&ball));
286+
.map(|ball| ball.coords.norm());
289287

290288
RoleState::assign_role(
291289
&mut commands,

yggdrasil/src/behavior/roles/striker.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ pub fn striker_role(
4141
ball_tracker: Res<BallTracker>,
4242
mut nao_manager: ResMut<NaoManager>,
4343
) {
44-
let Some(ball) = ball_tracker.stationary_ball() else {
44+
let Some(relative_ball) = ball_tracker.stationary_ball() else {
4545
commands.set_behavior(RlStrikerSearchBehavior);
4646
return;
4747
};
4848

49-
let relative_ball = pose.world_to_robot(&ball);
50-
let ball_angle = pose.angle_to(&ball);
51-
let ball_distance = pose.distance_to(&ball);
52-
let ball_target = Point3::new(ball.x, ball.y, 0.2);
49+
let absolute_ball = pose.robot_to_world(&relative_ball);
50+
let ball_angle = pose.angle_to(&absolute_ball);
51+
let ball_distance = relative_ball.coords.norm();
52+
let ball_target = Point3::new(absolute_ball.x, absolute_ball.y, 0.2);
5353

5454
let relative_goalpost_left =
5555
pose.world_to_robot(&Point2::new(layout_config.field.length / 2., 0.8));
@@ -89,7 +89,7 @@ pub fn striker_role(
8989
} else if ball_angle.abs() > WALK_WITH_BALL_ANGLE {
9090
nao_manager.set_right_eye_led(RightEye::fill(color::f32::PURPLE), Priority::default());
9191

92-
if relative_ball.y < 0. {
92+
if relative_ball.y.is_sign_negative() {
9393
commands.set_behavior(Walk {
9494
step: Step::RIGHT,
9595
look_target: Some(ball_target),

yggdrasil/src/game_controller/transmit.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub fn send_message(
6767
return;
6868
}
6969

70-
let (ball_age, ball_pos) = balls_to_game_controller_ball(&ball_tracker, &robot_pose);
70+
let (ball_age, ball_pos) = balls_to_game_controller_ball(&ball_tracker);
7171
let return_message = GameControllerReturnMessage::new(
7272
player_config.player_number,
7373
player_config.team_number,
@@ -94,17 +94,16 @@ fn robot_pose_to_game_controller_pose(robot_pose: &RobotPose) -> [f32; 3] {
9494
]
9595
}
9696

97-
fn balls_to_game_controller_ball(ball_tracker: &BallTracker, pose: &RobotPose) -> (f32, [f32; 2]) {
97+
fn balls_to_game_controller_ball(ball_tracker: &BallTracker) -> (f32, [f32; 2]) {
9898
let Some(ball) = ball_tracker.stationary_ball() else {
9999
return NO_BALL_DETECTED_DATA;
100100
};
101101

102-
let relative_ball = pose.world_to_robot(&ball);
103102
(
104103
ball_tracker.timestamp.elapsed().as_secs_f32(),
105104
[
106-
relative_ball.x * MILLIMETERS_PER_METER,
107-
relative_ball.y * MILLIMETERS_PER_METER,
105+
ball.x * MILLIMETERS_PER_METER,
106+
ball.y * MILLIMETERS_PER_METER,
108107
],
109108
)
110109
}

yggdrasil/src/vision/ball_detection/ball_tracker.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ use bevy::prelude::*;
44
use filter::{CovarianceMatrix, StateTransform, StateVector, UnscentedKalmanFilter};
55
use nalgebra::{point, Point2};
66

7-
use crate::nao::Cycle;
7+
use crate::{localization::odometry::Odometry, nao::Cycle};
88

99
#[derive(Debug)]
1010
pub enum BallHypothesis {
1111
Moving(f32),
1212
Stationary(f32),
1313
}
1414

15+
/// Filtered ball position in robot coordinates
1516
#[derive(Resource, Deref, DerefMut)]
1617
pub struct BallTracker {
1718
#[deref]
@@ -51,16 +52,21 @@ impl BallTracker {
5152
}
5253
}
5354

54-
pub fn predict(&mut self) {
55-
let f = |p: BallPosition| p;
56-
if let Err(err) = self.position_kf.predict(f, self.prediction_noise) {
55+
pub fn predict(&mut self, odometry: &Odometry) {
56+
// update last ball state with odometry, as we're moving relative to the ball.
57+
if let Err(err) = self.position_kf.predict(
58+
|p: BallPosition| BallPosition(odometry.offset_to_last.inverse() * p.0),
59+
self.prediction_noise,
60+
) {
5761
error!("failed to predict ball position: {err:?}");
5862
}
5963
}
6064

6165
pub fn measurement_update(&mut self, measurement: BallPosition) {
62-
let h = |p: BallPosition| p;
63-
if let Err(err) = self.position_kf.update(h, measurement, self.sensor_noise) {
66+
if let Err(err) =
67+
self.position_kf
68+
.update(std::convert::identity, measurement, self.sensor_noise)
69+
{
6470
error!("failed to do measurement update: {err:?}");
6571
}
6672

yggdrasil/src/vision/ball_detection/classifier.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use serde::{Deserialize, Serialize};
1414
use serde_with::{serde_as, DurationMicroSeconds};
1515

1616
use crate::core::debug::DebugContext;
17-
use crate::localization::RobotPose;
1817

18+
use crate::localization::odometry::Odometry;
1919
use crate::nao::Cycle;
2020
use crate::vision::camera::init_camera;
2121
use crate::vision::referee::detect::VisualRefereeDetectionStatus;
@@ -146,8 +146,8 @@ impl<T: CameraLocation> Clone for Ball<T> {
146146
}
147147

148148
/// System that runs the prediction step for the UKF backing the ball tracker.
149-
fn update_ball_tracker(mut ball_tracker: ResMut<BallTracker>) {
150-
ball_tracker.predict();
149+
fn update_ball_tracker(mut ball_tracker: ResMut<BallTracker>, odometry: Res<Odometry>) {
150+
ball_tracker.predict(&odometry);
151151
}
152152

153153
#[allow(clippy::too_many_arguments)]
@@ -160,7 +160,6 @@ fn classify_balls<T: CameraLocation>(
160160
mut ball_tracker: ResMut<BallTracker>,
161161
camera_matrix: Res<CameraMatrix<T>>,
162162
config: Res<BallDetectionConfig>,
163-
robot_pose: Res<RobotPose>,
164163
) {
165164
let classifier = &config.classifier;
166165
let start = Instant::now();
@@ -207,7 +206,7 @@ fn classify_balls<T: CameraLocation>(
207206
continue;
208207
};
209208

210-
let position = BallPosition(robot_pose.robot_to_world(&Point2::from(robot_to_ball.xy())));
209+
let position = BallPosition(robot_to_ball.xy());
211210

212211
confident_balls.push((position, confidence, proposal.clone()));
213212

yggdrasil/src/vision/ball_detection/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use serde_with::{serde_as, DurationMilliSeconds};
1919

2020
use crate::{
2121
core::debug::DebugContext,
22+
localization::RobotPose,
2223
nao::{Cycle, NaoManager, Priority},
2324
prelude::*,
2425
};
@@ -135,13 +136,14 @@ fn setup_3d_ball_debug_logging(dbg: DebugContext) {
135136
fn log_3d_balls(
136137
dbg: DebugContext,
137138
ball_tracker: Res<BallTracker>,
139+
robot_pose: Res<RobotPose>,
138140
mut last_logged: Local<Option<Cycle>>,
139141
) {
140142
let last_ball_tracker_update = ball_tracker.cycle;
141143
let state = ball_tracker.cutoff();
142144

143145
if let BallHypothesis::Stationary(max_variance) = state {
144-
let pos = ball_tracker.state();
146+
let pos = robot_pose.robot_to_world(&ball_tracker.state());
145147
if last_logged.is_none_or(|last_logged_cycle| last_ball_tracker_update > last_logged_cycle)
146148
{
147149
*last_logged = Some(last_ball_tracker_update);

0 commit comments

Comments
 (0)