Skip to content

Commit 44cc5be

Browse files
updates to iphone skill usage
1 parent c9ea8e0 commit 44cc5be

9 files changed

Lines changed: 1628 additions & 48 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ side-state-view.png
4040
top-down-state-view.png
4141

4242
# Jetbrains IDEs
43-
.idea/
43+
.idea/
44+
hand_image_failed_detection.png

predicators/pretrained_model_interface.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ def __init__(self, model_name: str) -> None:
233233
assert "GOOGLE_API_KEY" in os.environ
234234
self._client = genai.Client(api_key=os.getenv("GOOGLE_API_KEY"))
235235

236+
def _supports_thinking(self) -> bool:
237+
"""Check if this model version supports thinking (2.5+)."""
238+
import re
239+
match = re.search(r'(\d+\.\d+)', self._model_name)
240+
return match is not None and float(match.group(1)) >= 2.5
241+
236242

237243
class OpenAILLM(LargeLanguageModel, OpenAIModel):
238244
"""Interface to openAI LLMs.
@@ -293,9 +299,12 @@ def _sample_completions(
293299
num_completions: int = 1) -> List[str]: # pragma: no cover
294300
del seed, stop_token # unused
295301
assert imgs is None
296-
config = types.GenerateContentConfig(
297-
temperature=temperature,
298-
candidate_count=num_completions)
302+
config_kwargs = dict(temperature=temperature,
303+
candidate_count=num_completions)
304+
if self._supports_thinking():
305+
config_kwargs["thinking_config"] = types.ThinkingConfig(
306+
thinking_budget=0)
307+
config = types.GenerateContentConfig(**config_kwargs)
299308
with _GeminiTimeout(seconds=30):
300309
response = self._client.models.generate_content(
301310
model=self._model_name,
@@ -326,9 +335,12 @@ def _sample_completions(
326335
num_completions: int = 1) -> List[str]: # pragma: no cover
327336
del seed, stop_token # unused
328337
assert imgs is not None
329-
config = types.GenerateContentConfig(
330-
temperature=temperature,
331-
candidate_count=num_completions)
338+
config_kwargs = dict(temperature=temperature,
339+
candidate_count=num_completions)
340+
if self._supports_thinking():
341+
config_kwargs["thinking_config"] = types.ThinkingConfig(
342+
thinking_budget=0)
343+
config = types.GenerateContentConfig(**config_kwargs)
332344
with _GeminiTimeout(seconds=30):
333345
response = self._client.models.generate_content(
334346
model=self._model_name,

predicators/spot_utils/perception/object_detection.py

Lines changed: 130 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
from scipy import ndimage
3838
from 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+
4044
from predicators.pretrained_model_interface import GoogleGeminiVLM, \
4145
OpenAIVLM, VisionLanguageModel
4246
from 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+
647721
def 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

821915
def 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

Comments
 (0)