@@ -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+
4560pub 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+
237424fn 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