3737from scipy import ndimage
3838from scipy .spatial import Delaunay
3939
40+ from matplotlib .backends .backend_agg import FigureCanvasAgg as FigureCanvas
41+ from matplotlib .figure import Figure
42+ from matplotlib .patches import Rectangle
43+
4044from predicators .pretrained_model_interface import GoogleGeminiVLM , \
4145 OpenAIVLM , VisionLanguageModel
4246from predicators .settings import CFG
@@ -516,11 +520,12 @@ def _query_vlm(
516520 f"The images are:\n { img_desc_str } \n \n "
517521 f"Detect ONLY these objects: { class_names_str } .\n \n "
518522 f"Return a JSON object mapping camera names to arrays of detections. "
519- f"Each detection has class, bounding_box [x1, y1, x2, y2] in pixel "
520- f"coordinates for that image, and confidence (0.0 to 1.0).\n \n "
523+ f"Each detection has class, bounding_box as "
524+ f"[ymin, xmin, ymax, xmax] normalized to 0-1000, "
525+ f"and confidence (0.0 to 1.0).\n \n "
521526 f"Example:\n "
522- f'{{"camera_1": [{{"class": "cup", "bounding_box": [10, 20, 100 , '
523- f'200 ], "confidence": 0.9}}], "camera_2": []}}\n \n '
527+ f'{{"camera_1": [{{"class": "cup", "bounding_box": [100, 200, 500 , '
528+ f'800 ], "confidence": 0.9}}], "camera_2": []}}\n \n '
524529 f"Return ONLY JSON. Use the exact camera names shown above. "
525530 f"Use an empty array for cameras where no objects are detected."
526531 )
@@ -543,6 +548,8 @@ def _query_vlm(
543548 h , w = rgbd .rgb .shape [:2 ]
544549 image_rot = rgbd .image_rot
545550
551+ rot_w , rot_h = camera_dims [camera_name ]
552+
546553 for det in camera_detections :
547554 cls_name = det .get ("class" , "" )
548555 raw_box = det .get ("bounding_box" , [])
@@ -556,9 +563,17 @@ def _query_vlm(
556563 if rgbd .camera_name in object_id_to_img_detections [obj_id ]:
557564 continue
558565
566+ # Gemini returns [ymin, xmin, ymax, xmax] normalized to 0-1000.
567+ # Convert to pixel coords [x1, y1, x2, y2] in the rotated image.
568+ ymin_n , xmin_n , ymax_n , xmax_n = [float (v ) for v in raw_box ]
569+ rx1 = xmin_n / 1000.0 * rot_w
570+ ry1 = ymin_n / 1000.0 * rot_h
571+ rx2 = xmax_n / 1000.0 * rot_w
572+ ry2 = ymax_n / 1000.0 * rot_h
573+
559574 # Un-rotate the bounding box from rotated image space back to
560575 # original image space.
561- rot_box = tuple ( float ( v ) for v in raw_box )
576+ rot_box = ( rx1 , ry1 , rx2 , ry2 )
562577 box = _rotate_bounding_box (rot_box , - image_rot , h , w )
563578
564579 # Ensure x1 < x2, y1 < y2.
@@ -644,12 +659,74 @@ def _get_pose_from_segmented_bounding_box(
644659 return final_pose
645660
646661
662+ def _query_vlm_grasp_pixel (
663+ rgbds : Dict [str , RGBDImageWithContext ],
664+ object_id : LanguageObjectDetectionID ,
665+ camera_name : str ,
666+ ) -> Optional [Tuple [int , int ]]:
667+ """Query the VLM for the best grasp pixel for an object.
668+
669+ Returns (x, y) pixel coordinates or None if the query fails.
670+ """
671+ vlm = _get_vlm_model ()
672+ rgbd = rgbds [camera_name ]
673+ pil_img = PIL .Image .fromarray (rgbd .rotated_rgb ) # type: ignore
674+ w , h = pil_img .size
675+
676+ object_name = object_id .language_id
677+ prompt = (
678+ f"You are a robotic grasping system. You are given an image from a "
679+ f"robot's hand camera. The image contains an object "
680+ f"called \" { object_name } \" .\n \n "
681+ f"Identify the single best pixel coordinate to grasp this object. "
682+ f"Choose a point that is inside the object and would "
683+ f"allow a stable grasp.\n \n "
684+ f"Return ONLY a JSON object with keys \" y\" and \" x\" representing "
685+ f"the point coordinates normalized to 0-1000. "
686+ f"Example: {{\" y\" : 500, \" x\" : 500}}\n \n "
687+ f"Return ONLY JSON, no other text."
688+ )
689+
690+ try :
691+ completions = vlm .sample_completions (
692+ prompt , [pil_img ], temperature = 0.0 , seed = 0 , num_completions = 1 )
693+ response_text = completions [0 ].strip ()
694+ except Exception as e : # pylint: disable=broad-except
695+ logging .warning (f"VLM grasp pixel query failed: { e } " )
696+ return None
697+
698+ # Parse the response.
699+ text = response_text
700+ match = re .search (r'```(?:json)?\s*(.*?)\s*```' , text , re .DOTALL )
701+ if match :
702+ text = match .group (1 )
703+ try :
704+ parsed = json .loads (text )
705+ # Denormalize from 0-1000 to pixel coordinates.
706+ x = int (float (parsed ["x" ]) / 1000.0 * w )
707+ y = int (float (parsed ["y" ]) / 1000.0 * h )
708+ except (json .JSONDecodeError , KeyError , TypeError , ValueError ):
709+ logging .warning (f"VLM grasp pixel response could not be parsed: "
710+ f"{ response_text } " )
711+ return None
712+
713+ # Validate that the pixel is within image bounds.
714+ if 0 <= x < w and 0 <= y < h :
715+ return (x , y )
716+ logging .warning (f"VLM grasp pixel ({ x } , { y } ) out of bounds for "
717+ f"{ w } x{ h } image" )
718+ return None
719+
720+
647721def get_grasp_pixel (
648722 rgbds : Dict [str , RGBDImageWithContext ], artifacts : Dict [str , Any ],
649723 object_id : ObjectDetectionID , camera_name : str , rng : np .random .Generator
650724) -> Tuple [Tuple [int , int ], Optional [math_helpers .Quat ]]:
651725 """Select a pixel for grasping in the given camera image.
652726
727+ Uses a VLM to select the best grasp pixel for language-based detections.
728+ Falls back to random mask pixel selection if the VLM query fails.
729+
653730 NOTE: for april tag detections, the pixel returned will correspond to the
654731 center of the april tag, which may not always be ideal for grasping.
655732 Consider using OBJECT_SPECIFIC_GRASP_SELECTORS in this case.
@@ -659,6 +736,28 @@ def get_grasp_pixel(
659736 selector = OBJECT_SPECIFIC_GRASP_SELECTORS [object_id ]
660737 return selector (rgbds , artifacts , camera_name , rng )
661738
739+ # Try VLM-based grasp pixel selection for language detections.
740+ if isinstance (object_id , LanguageObjectDetectionID ):
741+ vlm_pixel = _query_vlm_grasp_pixel (rgbds , object_id , camera_name )
742+ if vlm_pixel is not None :
743+ logging .info (f"VLM selected grasp pixel { vlm_pixel } for "
744+ f"{ object_id .language_id } " )
745+ # Visualize the selected grasp pixel.
746+ fig = Figure ()
747+ FigureCanvas (fig )
748+ axes = fig .add_subplot (1 , 1 , 1 )
749+ rgb_img = rgbds [camera_name ].rotated_rgb
750+ axes .imshow (rgb_img )
751+ axes .plot (vlm_pixel [0 ], vlm_pixel [1 ], 'r+' , markersize = 15 ,
752+ markeredgewidth = 3 )
753+ axes .set_title (f"VLM grasp pixel for '{ object_id .language_id } '" )
754+ fig .tight_layout ()
755+ outdir = Path (CFG .spot_perception_outdir )
756+ outdir .mkdir (parents = True , exist_ok = True )
757+ fig .savefig (outdir / "vlm_grasp_pixel.png" , dpi = 300 )
758+ return vlm_pixel , None
759+
760+ # Fallback to random mask pixel selection.
662761 pixel = get_random_mask_pixel_from_artifacts (artifacts , object_id ,
663762 camera_name , rng )
664763 return (pixel [0 ], pixel [1 ]), None
@@ -730,12 +829,11 @@ def visualize_all_artifacts(artifacts: Dict[str,
730829 # duplicate first cols.
731830 fig_scale = 2
732831 if flat_detections :
733- _ , axes = plt .subplots (len (flat_detections ),
734- 5 ,
735- squeeze = False ,
736- figsize = (5 * fig_scale ,
737- len (flat_detections ) * fig_scale ))
738- plt .suptitle ("Detections" )
832+ fig = Figure (figsize = (5 * fig_scale ,
833+ len (flat_detections ) * fig_scale ))
834+ FigureCanvas (fig )
835+ axes = fig .subplots (len (flat_detections ), 5 , squeeze = False )
836+ fig .suptitle ("Detections" )
739837 for i , (rgbd , obj_id , seg_bb ) in enumerate (flat_detections ):
740838 ax_row = axes [i ]
741839 for ax in ax_row :
@@ -751,12 +849,12 @@ def visualize_all_artifacts(artifacts: Dict[str,
751849 x0 , y0 = box [0 ], box [1 ]
752850 w , h = box [2 ] - box [0 ], box [3 ] - box [1 ]
753851 ax_row [3 ].add_patch (
754- plt . Rectangle ((x0 , y0 ),
755- w ,
756- h ,
757- edgecolor = 'green' ,
758- facecolor = (0 , 0 , 0 , 0 ),
759- lw = 1 ))
852+ Rectangle ((x0 , y0 ),
853+ w ,
854+ h ,
855+ edgecolor = 'green' ,
856+ facecolor = (0 , 0 , 0 , 0 ),
857+ lw = 1 ))
760858
761859 ax_row [4 ].imshow (seg_bb .mask , cmap = "binary_r" , vmin = 0 , vmax = 1 )
762860
@@ -777,24 +875,21 @@ def visualize_all_artifacts(artifacts: Dict[str,
777875 ax_row [3 ].set_xlabel ("Bounding Box" )
778876 ax_row [4 ].set_xlabel ("Mask" )
779877
780- plt .tight_layout ()
781- plt .savefig (detections_outfile , dpi = 300 )
878+ fig .tight_layout ()
879+ fig .savefig (detections_outfile , dpi = 300 )
782880 print (f"Wrote out to { detections_outfile } ." )
783- plt .close ()
784881
785882 # Visualize all of the images that have no detections.
786883 all_cameras = set (rgbds )
787884 cameras_with_detections = {r .camera_name for r , _ , _ in flat_detections }
788885 cameras_without_detections = sorted (all_cameras - cameras_with_detections )
789886
790887 if cameras_without_detections :
791- _ , axes = plt .subplots (len (cameras_without_detections ),
792- 3 ,
793- squeeze = False ,
794- figsize = (3 * fig_scale ,
795- len (cameras_without_detections ) *
796- fig_scale ))
797- plt .suptitle ("Cameras without Detections" )
888+ fig = Figure (figsize = (3 * fig_scale ,
889+ len (cameras_without_detections ) * fig_scale ))
890+ FigureCanvas (fig )
891+ axes = fig .subplots (len (cameras_without_detections ), 3 , squeeze = False )
892+ fig .suptitle ("Cameras without Detections" )
798893 for i , camera in enumerate (cameras_without_detections ):
799894 rgbd = rgbds [camera ]
800895 ax_row = axes [i ]
@@ -812,10 +907,9 @@ def visualize_all_artifacts(artifacts: Dict[str,
812907 ax_row [1 ].set_xlabel ("Original RGB" )
813908 ax_row [2 ].set_xlabel ("Original Depth" )
814909
815- plt .tight_layout ()
816- plt .savefig (no_detections_outfile , dpi = 300 )
910+ fig .tight_layout ()
911+ fig .savefig (no_detections_outfile , dpi = 300 )
817912 print (f"Wrote out to { no_detections_outfile } ." )
818- plt .close ()
819913
820914
821915def display_camera_detections (artifacts : Dict [str , Any ],
@@ -866,12 +960,12 @@ def display_camera_detections(artifacts: Dict[str, Any],
866960 x0 , y0 = box [0 ], box [1 ]
867961 w , h = box [2 ] - box [0 ], box [3 ] - box [1 ]
868962 ax .add_patch (
869- plt . Rectangle ((x0 , y0 ),
870- w ,
871- h ,
872- edgecolor = color ,
873- facecolor = (0 , 0 , 0 , 0 ),
874- lw = 1 ))
963+ Rectangle ((x0 , y0 ),
964+ w ,
965+ h ,
966+ edgecolor = color ,
967+ facecolor = (0 , 0 , 0 , 0 ),
968+ lw = 1 ))
875969 # Label with the detection and score.
876970 ax .text (
877971 - 250 , # off to the left side
0 commit comments