@@ -83,6 +83,9 @@ def __init__(self):
8383 # Set up GStreamer streaming pipeline.
8484 self ._setup_gstreamer ()
8585
86+ # Set up Video Recorder if flag enabled.
87+ self ._setup_videosaver ()
88+
8689 def _declare_parameters (self ):
8790 self .declare_parameters (
8891 namespace = '' ,
@@ -159,6 +162,15 @@ def _load_calibration(self):
159162 # Use the vertical focal length (the [1,1] element) from the calibration file.
160163 self .goal_vertical_focal = self .left_camera_matrix [1 , 1 ]
161164
165+ self .h_fov = 2 * np .arctan (((self .left_image_size [1 ] / 2 ) / self .left_camera_matrix [0 ][0 ]))
166+ self .v_fov = 2 * np .arctan (((self .left_image_size [0 ] / 2 ) / self .left_camera_matrix [1 ][1 ]))
167+
168+ self .fx = self .left_camera_matrix [0 ][0 ]
169+ self .fy = self .left_camera_matrix [1 ][1 ]
170+
171+ self .cx = self .left_camera_matrix [0 ][2 ]
172+ self .cy = self .left_camera_matrix [1 ][2 ]
173+
162174 self .get_logger ().info ('Successfully loaded camera calibration files' )
163175 except Exception as e :
164176 self .get_logger ().error (f'Failed to load calibration files: { e } ' )
@@ -246,6 +258,23 @@ def _setup_gstreamer(self):
246258 self .stream_timestamp = 0.0
247259 self .frame_duration = 1.0 / 12.0
248260
261+ def _setup_videosaver (self ):
262+ if self .save_frames :
263+ # Ensure the save directory exists.
264+ os .makedirs (self .save_location , exist_ok = True )
265+ # Capture an initial frame to determine the frame size.
266+ ret , frame = self .capture_frame ()
267+ if ret :
268+ # Left frame dimensions: half the width of the full frame.
269+ frame_width = frame .shape [1 ] // 2
270+ frame_height = frame .shape [0 ]
271+ fourcc = cv2 .VideoWriter_fourcc (* 'XVID' )
272+ video_path = os .path .join (self .save_location , f'{ time .time ()} .avi' )
273+ self .video_writer = cv2 .VideoWriter (video_path , fourcc , 12 , (frame_width , frame_height ))
274+ self .get_logger ().info (f'Video recording initialized: { video_path } ' )
275+ else :
276+ self .get_logger ().error ('Failed to capture initial frame for video writer initialization' )
277+
249278 def capture_frame (self ):
250279 """Capture a frame from the camera."""
251280 return self .cap .read ()
@@ -398,6 +427,14 @@ def compute_grid_depths(self, disparity):
398427 grid_depths .append (depth )
399428 return grid_depths
400429
430+ def get_bbox_theta_offsets (self , bbox , depth ):
431+ offset_x = bbox [0 ] - self .cx
432+ offset_y = bbox [1 ] - self .cy
433+
434+ theta_x = np .arctan (offset_x / self .fx )
435+ theta_y = np .arctan (offset_y / self .fy )
436+ return theta_x , theta_y
437+
401438 def camera_callback (self ):
402439 """Main callback: capture, process, stream frames, and publish metrics and grid distances."""
403440 timing = {}
@@ -439,7 +476,7 @@ def camera_callback(self):
439476 if self .ball_search_mode :
440477 disp_depth = self .filter_disparity (disparity , detection_msg .bbox )
441478 regr_depth = self .mono_depth_estimator (detection_msg .bbox [2 ], detection_msg .bbox [3 ])
442- detection_msg .depth = np .min ([disp_depth , regr_depth ]) if (detection_msg .bbox [2 ]* detection_msg .bbox [3 ]) > 400 else 100.0
479+ detection_msg .depth = np .min ([disp_depth , regr_depth ]) if (np . square ( np . max ([ detection_msg .bbox [2 ], detection_msg .bbox [3 ]])) > 900.0 ) else 100.0
443480 else :
444481 # Goal detection mode: use the calibrated vertical focal length and real goal height.
445482 # Assume detection_msg.obj_class contains a string like "circle", "square", or "triangle"
@@ -450,18 +487,24 @@ def camera_callback(self):
450487 real_height = self .goal_triangle_height
451488 elif "square" in obj_class :
452489 real_height = self .goal_square_height
453-
490+ raw_goal_height = (self .goal_vertical_focal * real_height ) / detection_msg .bbox [3 ]
491+ raw_depth = (self .goal_vertical_focal * real_height ) / detection_msg .bbox [3 ]
454492 # Use the goal_vertical_focal loaded from calibration.
455- detection_msg .depth = ( self . goal_vertical_focal * real_height ) / detection_msg . bbox [ 3 ]
493+ detection_msg .depth = 0.31804 * np . exp ( 0.59 * raw_depth ) + 1.424
456494
495+ theta_x , theta_y = self .get_bbox_theta_offsets (detection_msg .bbox , detection_msg .depth )
457496 self .pub_detections .publish (Float64MultiArray (data = [
458497 detection_msg .bbox [0 ],
459498 detection_msg .bbox [1 ],
460499 detection_msg .depth ,
461500 detection_msg .track_id * 1.0 ,
462- (not self .ball_search_mode ) * 1.0
501+ (not self .ball_search_mode ) * 1.0 ,
502+ theta_x ,
503+ theta_y
463504 ]))
464505
506+
507+
465508 # Prepare debug view.
466509 debug_view = left_frame .copy ()
467510 if detection_msg is not None :
@@ -496,6 +539,9 @@ def camera_callback(self):
496539 perf_msg .fps = (1 / perf_msg .total_time ) * 1000
497540 self .pub_performance .publish (perf_msg )
498541
542+ if self .save_frames :
543+ self .video_writer .write (left_frame )
544+
499545
500546def main (args = None ):
501547 import rclpy
@@ -506,6 +552,8 @@ def main(args=None):
506552 except KeyboardInterrupt :
507553 pass
508554 finally :
555+ if node .save_frames :
556+ node .video_writer .release ()
509557 node .thread_pool .shutdown (wait = True )
510558 node .destroy_node ()
511559 rclpy .shutdown ()
0 commit comments