Skip to content

Commit 7110b08

Browse files
committed
Add ball filter cache diagnostics
1 parent 7b816df commit 7110b08

3 files changed

Lines changed: 233 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/nodes/ball_filter/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ ros-z = { workspace = true }
2222
ros-z-streams = { workspace = true }
2323
serde = { workspace = true, features = ["derive"] }
2424
tokio = { workspace = true }
25+
tracing = { workspace = true }
2526
types = { workspace = true }

crates/nodes/ball_filter/src/lib.rs

Lines changed: 231 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ struct BallFilterOutput {
4242
hypothetical_ball_positions: Vec<HypotheticalBallPosition<Ground>>,
4343
}
4444

45+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46+
enum CameraMatrixCacheQueryPosition {
47+
Before,
48+
Within,
49+
After,
50+
}
51+
52+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53+
struct CameraMatrixCacheDiagnostics {
54+
query_position: CameraMatrixCacheQueryPosition,
55+
selected_delta_ns: i64,
56+
retained_window: Duration,
57+
estimated_required_capacity: Option<usize>,
58+
}
59+
4560
pub fn run_boxed(ctx: Arc<Context>) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
4661
Box::pin(run(ctx))
4762
}
@@ -142,10 +157,19 @@ pub async fn run(ctx: Arc<Context>) -> Result<()> {
142157
}
143158

144159
if let Some(detected_objects) = detected_objects {
145-
let timed_camera_matrix = camera_matrix_cache.get_nearest(time);
160+
let timed_camera_matrix = camera_matrix_cache.get_nearest_with_stamp(time);
146161
let camera_matrix = timed_camera_matrix
147162
.as_ref()
148-
.map(|camera_matrix| &camera_matrix.inner);
163+
.map(|(_, camera_matrix)| &camera_matrix.inner);
164+
log_camera_matrix_cache_lookup(
165+
"detected_objects",
166+
time,
167+
timed_camera_matrix.as_ref().map(|(stamp, _)| *stamp),
168+
camera_matrix,
169+
camera_matrix_cache.earliest_stamp(),
170+
camera_matrix_cache.latest_stamp(),
171+
camera_matrix_cache.len(),
172+
);
149173
let Some(projected_balls) = project_detected_balls(
150174
Some(&detected_objects),
151175
camera_matrix,
@@ -154,6 +178,7 @@ pub async fn run(ctx: Arc<Context>) -> Result<()> {
154178
) else {
155179
continue;
156180
};
181+
log_projected_ball_percepts("detected_objects", time, &projected_balls);
157182

158183
ball_percepts.extend_from_slice(&projected_balls);
159184

@@ -198,10 +223,25 @@ pub async fn run(ctx: Arc<Context>) -> Result<()> {
198223
.collect();
199224

200225
let ball_radius = field_dimensions.ball_radius;
201-
let filtered_balls_in_image = if let Some(time) = projection_time
202-
&& let Some(timed_camera_matrix) = camera_matrix_cache.get_nearest(time)
203-
{
204-
project_to_image(&output_balls, &timed_camera_matrix.inner, ball_radius)
226+
let filtered_balls_in_image = if let Some(time) = projection_time {
227+
let timed_camera_matrix = camera_matrix_cache.get_nearest_with_stamp(time);
228+
let camera_matrix = timed_camera_matrix
229+
.as_ref()
230+
.map(|(_, camera_matrix)| &camera_matrix.inner);
231+
log_camera_matrix_cache_lookup(
232+
"filtered_balls_in_image",
233+
time,
234+
timed_camera_matrix.as_ref().map(|(stamp, _)| *stamp),
235+
camera_matrix,
236+
camera_matrix_cache.earliest_stamp(),
237+
camera_matrix_cache.latest_stamp(),
238+
camera_matrix_cache.len(),
239+
);
240+
camera_matrix
241+
.map(|camera_matrix| {
242+
project_to_image(&output_balls, camera_matrix, ball_radius)
243+
})
244+
.unwrap_or_default()
205245
} else {
206246
vec![]
207247
};
@@ -234,6 +274,153 @@ pub async fn run(ctx: Arc<Context>) -> Result<()> {
234274
}
235275
}
236276

277+
fn log_camera_matrix_cache_lookup(
278+
context: &str,
279+
query_time: Time,
280+
selected_stamp: Option<Time>,
281+
selected_camera_matrix: Option<&CameraMatrix>,
282+
earliest_stamp: Option<Time>,
283+
latest_stamp: Option<Time>,
284+
cache_len: usize,
285+
) {
286+
let Some(selected_stamp) = selected_stamp else {
287+
tracing::debug!(
288+
target: "ball_filter::camera_matrix_cache",
289+
context,
290+
query_time_ns = query_time.as_nanos(),
291+
cache_earliest_ns = ?earliest_stamp.map(Time::as_nanos),
292+
cache_latest_ns = ?latest_stamp.map(Time::as_nanos),
293+
cache_len,
294+
"camera matrix cache lookup returned no sample"
295+
);
296+
return;
297+
};
298+
let (Some(earliest_stamp), Some(latest_stamp)) = (earliest_stamp, latest_stamp) else {
299+
tracing::debug!(
300+
target: "ball_filter::camera_matrix_cache",
301+
context,
302+
query_time_ns = query_time.as_nanos(),
303+
selected_stamp_ns = selected_stamp.as_nanos(),
304+
selected_delta_ms = selected_stamp
305+
.as_nanos()
306+
.saturating_sub(query_time.as_nanos()) as f64
307+
/ 1_000_000.0,
308+
cache_len,
309+
"camera matrix cache lookup has selected sample but incomplete cache window"
310+
);
311+
return;
312+
};
313+
314+
let diagnostics = camera_matrix_cache_diagnostics(
315+
query_time,
316+
selected_stamp,
317+
earliest_stamp,
318+
latest_stamp,
319+
cache_len,
320+
);
321+
let (head_roll, head_pitch, head_yaw) = selected_camera_matrix
322+
.map(camera_matrix_head_euler_angles)
323+
.map_or((None, None, None), |(roll, pitch, yaw)| {
324+
(Some(roll), Some(pitch), Some(yaw))
325+
});
326+
327+
tracing::debug!(
328+
target: "ball_filter::camera_matrix_cache",
329+
context,
330+
query_time_ns = query_time.as_nanos(),
331+
selected_stamp_ns = selected_stamp.as_nanos(),
332+
selected_delta_ms = diagnostics.selected_delta_ns as f64 / 1_000_000.0,
333+
cache_earliest_ns = earliest_stamp.as_nanos(),
334+
cache_latest_ns = latest_stamp.as_nanos(),
335+
retained_window_ms = diagnostics.retained_window.as_secs_f64() * 1000.0,
336+
cache_len,
337+
query_position = ?diagnostics.query_position,
338+
estimated_required_capacity = ?diagnostics.estimated_required_capacity,
339+
head_roll = ?head_roll,
340+
head_pitch = ?head_pitch,
341+
head_yaw = ?head_yaw,
342+
"camera matrix cache nearest lookup"
343+
);
344+
}
345+
346+
fn camera_matrix_cache_diagnostics(
347+
query_time: Time,
348+
selected_stamp: Time,
349+
earliest_stamp: Time,
350+
latest_stamp: Time,
351+
cache_len: usize,
352+
) -> CameraMatrixCacheDiagnostics {
353+
let query_position = if query_time < earliest_stamp {
354+
CameraMatrixCacheQueryPosition::Before
355+
} else if query_time > latest_stamp {
356+
CameraMatrixCacheQueryPosition::After
357+
} else {
358+
CameraMatrixCacheQueryPosition::Within
359+
};
360+
361+
CameraMatrixCacheDiagnostics {
362+
query_position,
363+
selected_delta_ns: selected_stamp
364+
.as_nanos()
365+
.saturating_sub(query_time.as_nanos()),
366+
retained_window: latest_stamp.duration_since(earliest_stamp),
367+
estimated_required_capacity: estimate_required_camera_matrix_cache_capacity(
368+
query_time,
369+
earliest_stamp,
370+
latest_stamp,
371+
cache_len,
372+
),
373+
}
374+
}
375+
376+
fn estimate_required_camera_matrix_cache_capacity(
377+
query_time: Time,
378+
earliest_stamp: Time,
379+
latest_stamp: Time,
380+
cache_len: usize,
381+
) -> Option<usize> {
382+
if query_time >= earliest_stamp || cache_len < 2 {
383+
return None;
384+
}
385+
386+
let retained_window_ns = latest_stamp.duration_since(earliest_stamp).as_nanos();
387+
if retained_window_ns == 0 {
388+
return None;
389+
}
390+
391+
let required_window_ns = latest_stamp.duration_since(query_time).as_nanos();
392+
let retained_intervals = (cache_len - 1) as u128;
393+
let required_intervals = required_window_ns
394+
.checked_mul(retained_intervals)?
395+
.div_ceil(retained_window_ns);
396+
397+
usize::try_from(required_intervals.checked_add(1)?).ok()
398+
}
399+
400+
fn camera_matrix_head_euler_angles(camera_matrix: &CameraMatrix) -> (f32, f32, f32) {
401+
camera_matrix
402+
.robot_to_head
403+
.rotation()
404+
.as_orientation()
405+
.euler_angles()
406+
}
407+
408+
fn log_projected_ball_percepts(context: &str, time: Time, ball_percepts: &[BallPercept]) {
409+
for ball_percept in ball_percepts {
410+
tracing::debug!(
411+
target: "ball_filter::projection",
412+
context,
413+
time_ns = time.as_nanos(),
414+
ground_x = ball_percept.percept_in_ground.mean[0],
415+
ground_y = ball_percept.percept_in_ground.mean[1],
416+
image_x = ball_percept.image_location.center.x(),
417+
image_y = ball_percept.image_location.center.y(),
418+
image_radius = ball_percept.image_location.radius,
419+
"projected ball percept"
420+
);
421+
}
422+
}
423+
237424
fn predict_hypotheses_from_odometry(
238425
ball_filter: &mut BallFilter,
239426
time: Time,
@@ -604,4 +791,42 @@ mod tests {
604791
"uncertain percept should not be treated as a precise outlier, got cost {cost}"
605792
);
606793
}
794+
795+
#[test]
796+
fn camera_matrix_cache_diagnostics_recommends_capacity_for_query_before_cache_window() {
797+
let diagnostics = camera_matrix_cache_diagnostics(
798+
Time::from_nanos(1_000_000_000),
799+
Time::from_nanos(1_100_000_000),
800+
Time::from_nanos(1_100_000_000),
801+
Time::from_nanos(1_400_000_000),
802+
4,
803+
);
804+
805+
assert_eq!(
806+
diagnostics.query_position,
807+
CameraMatrixCacheQueryPosition::Before
808+
);
809+
assert_eq!(diagnostics.selected_delta_ns, 100_000_000);
810+
assert_eq!(diagnostics.retained_window, Duration::from_millis(300));
811+
assert_eq!(diagnostics.estimated_required_capacity, Some(5));
812+
}
813+
814+
#[test]
815+
fn camera_matrix_cache_diagnostics_reports_future_query_without_capacity_estimate() {
816+
let diagnostics = camera_matrix_cache_diagnostics(
817+
Time::from_nanos(1_500_000_000),
818+
Time::from_nanos(1_400_000_000),
819+
Time::from_nanos(1_100_000_000),
820+
Time::from_nanos(1_400_000_000),
821+
4,
822+
);
823+
824+
assert_eq!(
825+
diagnostics.query_position,
826+
CameraMatrixCacheQueryPosition::After
827+
);
828+
assert_eq!(diagnostics.selected_delta_ns, -100_000_000);
829+
assert_eq!(diagnostics.retained_window, Duration::from_millis(300));
830+
assert_eq!(diagnostics.estimated_required_capacity, None);
831+
}
607832
}

0 commit comments

Comments
 (0)