@@ -90,7 +90,12 @@ def __init__(
9090 self .orb = cv2 .ORB_create ()
9191 self .bf = cv2 .BFMatcher (cv2 .NORM_HAMMING , crossCheck = False )
9292 self .original_des = None # Store original ORB descriptors
93+ self .original_kps = None # Store original ORB keypoints
9394 self .reid_fail_count = 0 # Counter for consecutive re-id failures
95+ self .last_good_matches = [] # Store good matches for visualization
96+ self .last_roi_kps = None # Store last ROI keypoints for visualization
97+ self .last_roi_bbox = None # Store last ROI bbox for visualization
98+ self .reid_confirmed = False # Store current reid confirmation state
9499
95100 # For tracking latest frame data
96101 self ._latest_rgb_frame : Optional [np .ndarray ] = None
@@ -182,7 +187,7 @@ def track(
182187 # Extract initial features
183188 roi = self ._latest_rgb_frame [y1 :y2 , x1 :x2 ]
184189 if roi .size > 0 :
185- _ , self .original_des = self .orb .detectAndCompute (roi , None )
190+ self . original_kps , self .original_des = self .orb .detectAndCompute (roi , None )
186191 if self .original_des is None :
187192 logger .warning ("No ORB features found in initial ROI." )
188193 self .stop_track ()
@@ -217,23 +222,31 @@ def reid(self, frame, current_bbox) -> bool:
217222 if roi .size == 0 :
218223 return False # Empty ROI cannot match
219224
220- _ , des_current = self .orb .detectAndCompute (roi , None )
225+ kps_current , des_current = self .orb .detectAndCompute (roi , None )
221226 if des_current is None or len (des_current ) < 2 :
222227 return False # Need at least 2 descriptors for knnMatch
223228
229+ # Store ROI keypoints and bbox for visualization
230+ self .last_roi_kps = kps_current
231+ self .last_roi_bbox = [x1 , y1 , x2 , y2 ]
232+
224233 # Handle case where original_des has only 1 descriptor (cannot use knnMatch with k=2)
225234 if len (self .original_des ) < 2 :
226235 matches = self .bf .match (self .original_des , des_current )
236+ self .last_good_matches = matches # Store all matches for visualization
227237 good_matches = len (matches )
228238 else :
229239 matches = self .bf .knnMatch (self .original_des , des_current , k = 2 )
230240 # Apply Lowe's ratio test robustly
241+ good_matches_list = []
231242 good_matches = 0
232243 for match_pair in matches :
233244 if len (match_pair ) == 2 :
234245 m , n = match_pair
235246 if m .distance < 0.75 * n .distance :
247+ good_matches_list .append (m )
236248 good_matches += 1
249+ self .last_good_matches = good_matches_list # Store good matches for visualization
237250
238251 return good_matches >= self .reid_threshold
239252
@@ -261,7 +274,12 @@ def _reset_tracking_state(self):
261274 self .tracking_bbox = None
262275 self .tracking_initialized = False
263276 self .original_des = None
277+ self .original_kps = None
264278 self .reid_fail_count = 0 # Reset counter
279+ self .last_good_matches = []
280+ self .last_roi_kps = None
281+ self .last_roi_bbox = None
282+ self .reid_confirmed = False # Reset reid confirmation state
265283
266284 # Publish empty detections to clear any visualizations
267285 empty_2d = Detection2DArray (detections_length = 0 , header = Header (), detections = [])
@@ -298,6 +316,16 @@ def stop_track(self) -> bool:
298316 logger .info ("Tracking stopped" )
299317 return True
300318
319+ @rpc
320+ def is_tracking (self ) -> bool :
321+ """
322+ Check if the tracker is currently tracking an object successfully.
323+
324+ Returns:
325+ bool: True if tracking is active and REID is confirmed, False otherwise
326+ """
327+ return self .tracking_initialized and self .reid_confirmed
328+
301329 def _process_tracking (self ):
302330 """Process current frame for tracking and publish detections."""
303331 if self ._latest_rgb_frame is None or self .tracker is None or not self .tracking_initialized :
@@ -316,11 +344,14 @@ def _process_tracking(self):
316344 current_bbox_x1y1x2y2 = [x , y , x + w , y + h ]
317345 # Perform re-ID check
318346 reid_confirmed_this_frame = self .reid (frame , current_bbox_x1y1x2y2 )
347+ self .reid_confirmed = reid_confirmed_this_frame # Store for is_tracking() RPC
319348
320349 if reid_confirmed_this_frame :
321350 self .reid_fail_count = 0
322351 else :
323352 self .reid_fail_count += 1
353+ else :
354+ self .reid_confirmed = False # No tracking if tracker failed
324355
325356 # Determine final success
326357 if tracker_succeeded :
@@ -480,10 +511,53 @@ def _process_tracking(self):
480511 self ._latest_rgb_frame , detections_3d , show_coordinates = True , bboxes_2d = bbox_2d
481512 )
482513
514+ # Overlay REID feature matches if available
515+ if self .last_good_matches and self .last_roi_kps and self .last_roi_bbox :
516+ viz_image = self ._draw_reid_matches (viz_image )
517+
483518 # Convert to Image message and publish
484519 viz_msg = Image .from_numpy (viz_image )
485520 self .tracked_overlay .publish (viz_msg )
486521
522+ def _draw_reid_matches (self , image : np .ndarray ) -> np .ndarray :
523+ """Draw REID feature matches on the image."""
524+ viz_image = image .copy ()
525+
526+ x1 , y1 , x2 , y2 = self .last_roi_bbox
527+
528+ # Draw keypoints from current ROI in green
529+ for kp in self .last_roi_kps :
530+ pt = (int (kp .pt [0 ] + x1 ), int (kp .pt [1 ] + y1 ))
531+ cv2 .circle (viz_image , pt , 3 , (0 , 255 , 0 ), - 1 )
532+
533+ for match in self .last_good_matches :
534+ current_kp = self .last_roi_kps [match .trainIdx ]
535+ pt_current = (int (current_kp .pt [0 ] + x1 ), int (current_kp .pt [1 ] + y1 ))
536+
537+ # Draw a larger circle for matched points in yellow
538+ cv2 .circle (viz_image , pt_current , 5 , (0 , 255 , 255 ), 2 ) # Yellow for matched points
539+
540+ # Draw match strength indicator (smaller circle with intensity based on distance)
541+ # Lower distance = better match = brighter color
542+ intensity = int (255 * (1.0 - min (match .distance / 100.0 , 1.0 )))
543+ cv2 .circle (viz_image , pt_current , 2 , (intensity , intensity , 255 ), - 1 )
544+
545+ text = f"REID Matches: { len (self .last_good_matches )} /{ len (self .last_roi_kps ) if self .last_roi_kps else 0 } "
546+ cv2 .putText (viz_image , text , (10 , 30 ), cv2 .FONT_HERSHEY_SIMPLEX , 0.7 , (0 , 255 , 255 ), 2 )
547+
548+ if len (self .last_good_matches ) >= self .reid_threshold :
549+ status_text = "REID: CONFIRMED"
550+ status_color = (0 , 255 , 0 ) # Green
551+ else :
552+ status_text = f"REID: WEAK ({ self .reid_fail_count } /{ self .reid_fail_tolerance } )"
553+ status_color = (0 , 165 , 255 ) # Orange
554+
555+ cv2 .putText (
556+ viz_image , status_text , (10 , 60 ), cv2 .FONT_HERSHEY_SIMPLEX , 0.7 , status_color , 2
557+ )
558+
559+ return viz_image
560+
487561 def _get_depth_from_bbox (self , bbox : List [int ]) -> Optional [float ]:
488562 """Calculate depth from bbox using the 25th percentile of closest points."""
489563 if self ._latest_depth_frame is None :
@@ -504,8 +578,6 @@ def _get_depth_from_bbox(self, bbox: List[int]) -> Optional[float]:
504578 valid_depths = roi_depth [np .isfinite (roi_depth ) & (roi_depth > 0 )]
505579
506580 if len (valid_depths ) > 0 :
507- # Take the 25th percentile of the closest (smallest) depth values
508- # This helps get a robust depth estimate for the front surface of the object
509581 depth_25th_percentile = float (np .percentile (valid_depths , 25 ))
510582 return depth_25th_percentile
511583
0 commit comments