Skip to content

Commit d68071c

Browse files
committed
major update: add an aggregated perception monitor to decide when to call perception pipelines
1 parent 9c9e87c commit d68071c

2 files changed

Lines changed: 206 additions & 85 deletions

File tree

predicators/envs/spot_env.py

Lines changed: 124 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
Predicate, Action, GoalDescription, EnvironmentTask, STRIPSOperator, \
3434
VLMPredicate, VLMGroundAtom, Observation, Type, SpotActionExtraInfo, \
3535
LiftedAtom, Variable
36+
from predicators.perception.perception_monitor import PerceptionDecision, \
37+
PerceptionMonitor
3638
from predicators.spot_utils.perception.object_detection import \
3739
AprilTagObjectDetectionID, KnownStaticObjectDetectionID, \
3840
LanguageObjectDetectionID, ObjectDetectionID, detect_objects_from_language, \
@@ -75,6 +77,11 @@
7577
_obj_name_to_sim_obj: Dict[str, pbrspot.body.Body] = {}
7678

7779

80+
def _is_hand_view_action(action_name: str) -> bool:
81+
"""Detect whether the operator causes a hand-view observation."""
82+
return "HandView" in action_name
83+
84+
7885
@dataclass(frozen=True)
7986
class _SpotObservation:
8087
"""An observation for a SpotEnv."""
@@ -213,7 +220,8 @@ def get_robot(
213220
lease_client.take()
214221
lease_keepalive = LeaseKeepAlive(lease_client,
215222
must_acquire=True,
216-
return_at_exit=True)
223+
return_at_exit=True,
224+
warnings=False)
217225
localizer = None
218226
assert path.exists()
219227
if use_localizer:
@@ -331,6 +339,7 @@ def __init__(self, use_gui: bool = True) -> None:
331339
self._cached_realworld_observation: Optional[_SpotObservation] = None
332340
self._last_vlm_pointing: Optional[
333341
vlm_pointing.VLMPointingResult] = None
342+
self._perception_monitor = PerceptionMonitor()
334343

335344
def _initialize_pybullet(self) -> None:
336345
# First, check if we have any connections to pybullet already,
@@ -515,7 +524,7 @@ def _maybe_trigger_vlm_pointing(self, action_name: str,
515524
action_objs: Sequence[Object],
516525
observation: _SpotObservation) -> None:
517526
"""Optionally compute a hand-camera grasp point via Gemini pointing."""
518-
if not CFG.spot_use_object_pointing:
527+
if not CFG.spot_use_vlm_pointing:
519528
self._last_vlm_pointing = None
520529
return
521530

@@ -777,6 +786,11 @@ def reset(self, train_or_test: str, task_idx: int) -> Observation:
777786
while True:
778787
try:
779788
self._lease_client.take()
789+
if CFG.spot_enable_manual_reset_teleop and \
790+
not CFG.spot_run_dry:
791+
logging.info(
792+
"[SpotEnv] Manual reset enabled; skipping automatic"
793+
" stow/go-home motions before initial observation.")
780794
self._current_task = self._actively_construct_env_task()
781795
break
782796
except RetryableRpcError as e:
@@ -790,6 +804,8 @@ def reset(self, train_or_test: str, task_idx: int) -> Observation:
790804
self._cached_realworld_observation = self._current_observation
791805
else:
792806
self._cached_realworld_observation = None
807+
self._perception_monitor.reset(
808+
initial_scan_done=self._cached_realworld_observation is not None)
793809
self._current_task_goal_reached = False
794810
self._last_action = None
795811

@@ -900,96 +916,104 @@ def step(self, action: Action) -> Observation:
900916
logging.warning("WARNING: the following retryable error "
901917
f"was encountered. Trying again.\n{e}")
902918

903-
observe_only_refresh = CFG.spot_perception_refresh_observe_only
904-
force_skip_perception = observe_only_refresh and not is_info_action
905-
cached_available = self._cached_realworld_observation is not None
906-
skip_perception = False
919+
decision = self._perception_monitor.decide(
920+
has_cached_observation=self._cached_realworld_observation
921+
is not None,
922+
is_information_gathering=is_info_action,
923+
is_hand_view_action=_is_hand_view_action(action_name))
924+
logging.debug("[SpotEnv] Perception decision: %s",
925+
decision.reason)
907926

908-
if force_skip_perception:
909-
if cached_available:
910-
skip_perception = True
911-
else:
912-
logging.debug(
913-
"[SpotEnv] Observe-only perception requested but no "
914-
"cached observation is available; running perception "
915-
"once to seed cache.")
916-
elif CFG.spot_skip_perception_for_manipulation and \
917-
not is_info_action and cached_available:
918-
skip_perception = True
919-
920-
if skip_perception:
927+
if decision.refresh_full:
928+
while True:
929+
try:
930+
next_obs = self._build_realworld_observation(
931+
next_nonpercept, curr_obs=obs)
932+
self._perception_monitor.mark_refresh_complete()
933+
self._cached_realworld_observation = next_obs
934+
break
935+
except RetryableRpcError as e:
936+
logging.warning(
937+
"WARNING: the following retryable error "
938+
f"was encountered. Trying again.\n{e}")
939+
elif decision.reuse_cache and \
940+
self._cached_realworld_observation is not None:
921941
next_obs = self._reuse_cached_observation(next_nonpercept)
922-
self._cached_realworld_observation = next_obs
923942
else:
924-
# Get the new observation. Again, automatically retry if needed.
943+
logging.debug(
944+
"[SpotEnv] No cached observation available; running "
945+
"perception to seed cache.")
925946
while True:
926947
try:
927948
next_obs = self._build_realworld_observation(
928949
next_nonpercept, curr_obs=obs)
950+
self._perception_monitor.mark_refresh_complete()
951+
self._cached_realworld_observation = next_obs
929952
break
930953
except RetryableRpcError as e:
931954
logging.warning(
932955
"WARNING: the following retryable error "
933956
f"was encountered. Trying again.\n{e}")
934957

958+
if decision.trigger_pointing:
935959
self._maybe_trigger_vlm_pointing(action_name, action_objs,
936960
next_obs)
937961

938-
# Very hacky optimization to force viewing/reaching to work.
939-
# NOTE: enter the check if "move to" "object"; make sure op names in such pattern are always needed to check
940-
if CFG.spot_enable_interactive_checks and \
941-
"MoveTo" in action_name and "Object" in action_name:
962+
# Very hacky optimization to force viewing/reaching to work.
963+
# NOTE: enter the check if "move to" "object"; make sure op names in such pattern are always needed to check
964+
if CFG.spot_enable_interactive_checks and \
965+
"MoveTo" in action_name and "Object" in action_name:
966+
logging.warning(
967+
f"Entering object detection check with action_name: {action_name}")
968+
_, target_obj = action_objs
969+
# Retry if each of the types of moving failed in their own way.
970+
if action_name == "MoveToHandViewObject" or "Hand" in action_name:
971+
need_retry = target_obj not in \
972+
next_obs.objects_in_hand_view
973+
elif action_name == "MoveToBodyViewObject":
974+
need_retry = target_obj not in \
975+
next_obs.objects_in_any_view_except_back
976+
elif action_name == "MoveToReachObject":
977+
obj_pose = self._last_known_object_poses[target_obj]
978+
obj_position = math_helpers.Vec3(x=obj_pose.x,
979+
y=obj_pose.y,
980+
z=obj_pose.z)
981+
need_retry = not _obj_reachable_from_spot_pose(
982+
next_obs.robot_pos, obj_position)
983+
else:
984+
need_retry = False
985+
logging.info("WARNING: object detection check not "
986+
"implemented for this action: %s",
987+
action_name)
988+
if need_retry:
942989
logging.warning(
943-
f"Entering object detection check with action_name: {action_name}")
944-
_, target_obj = action_objs
945-
# Retry if each of the types of moving failed in their own way.
946-
if action_name == "MoveToHandViewObject" or "Hand" in action_name:
947-
need_retry = target_obj not in \
948-
next_obs.objects_in_hand_view
949-
elif action_name == "MoveToBodyViewObject":
950-
need_retry = target_obj not in \
951-
next_obs.objects_in_any_view_except_back
952-
elif action_name == "MoveToReachObject":
953-
obj_pose = self._last_known_object_poses[target_obj]
954-
obj_position = math_helpers.Vec3(x=obj_pose.x,
955-
y=obj_pose.y,
956-
z=obj_pose.z)
957-
need_retry = not _obj_reachable_from_spot_pose(
958-
next_obs.robot_pos, obj_position)
959-
else:
960-
need_retry = False
961-
logging.info("WARNING: object detection check not "
962-
"implemented for this action: %s",
963-
action_name)
964-
if need_retry:
965-
logging.warning(
966-
"WARNING: retrying %s because %s was not "
967-
"seen/reached.", action_name, target_obj)
968-
prompt = (
969-
"Hit 'c' to have the robot do a random movement "
970-
"or take control and move the robot accordingly. "
971-
"Hit the 'Enter' key when you're done!\n")
972-
user_pref = input(prompt)
973-
assert self._lease_client is not None
974-
self._lease_client.take()
975-
angle = 0.0
976-
if user_pref == "c":
977-
# Do a small random movement to get a new view.
978-
assert isinstance(action_fn_args[1],
979-
math_helpers.SE2Pose)
980-
angle = self._noise_rng.uniform(-np.pi / 6,
981-
np.pi / 6)
982-
rel_pose = math_helpers.SE2Pose(0, 0, angle)
983-
assert isinstance(action_fn_args, tuple)
984-
new_action_args = action_fn_args[0:1] + (rel_pose, ) + \
985-
action_fn_args[2:]
986-
987-
new_action = utils.create_spot_env_action(
988-
SpotActionExtraInfo(action_name, action_objs,
989-
action_fn, new_action_args,
990-
sim_action_fn,
991-
sim_action_args))
992-
return self.step(new_action)
990+
"WARNING: retrying %s because %s was not "
991+
"seen/reached.", action_name, target_obj)
992+
prompt = (
993+
"Hit 'c' to have the robot do a random movement "
994+
"or take control and move the robot accordingly. "
995+
"Hit the 'Enter' key when you're done!\n")
996+
user_pref = input(prompt)
997+
assert self._lease_client is not None
998+
self._lease_client.take()
999+
angle = 0.0
1000+
if user_pref == "c":
1001+
# Do a small random movement to get a new view.
1002+
assert isinstance(action_fn_args[1],
1003+
math_helpers.SE2Pose)
1004+
angle = self._noise_rng.uniform(-np.pi / 6,
1005+
np.pi / 6)
1006+
rel_pose = math_helpers.SE2Pose(0, 0, angle)
1007+
assert isinstance(action_fn_args, tuple)
1008+
new_action_args = action_fn_args[0:1] + (rel_pose, ) + \
1009+
action_fn_args[2:]
1010+
1011+
new_action = utils.create_spot_env_action(
1012+
SpotActionExtraInfo(action_name, action_objs,
1013+
action_fn, new_action_args,
1014+
sim_action_fn,
1015+
sim_action_args))
1016+
return self.step(new_action)
9931017
self._allow_incidental_discovery = False
9941018

9951019
self._current_observation = next_obs
@@ -1464,13 +1488,22 @@ def _actively_construct_initial_object_views(
14641488
bool or None]]:
14651489
assert self._robot is not None
14661490
assert self._localizer is not None
1467-
stow_arm(self._robot)
1468-
go_home(self._robot, self._localizer)
1491+
skip_motion = (CFG.spot_enable_manual_reset_teleop
1492+
and not CFG.spot_run_dry)
1493+
if skip_motion:
1494+
logging.info(
1495+
"[SpotEnv] Manual reset enabled; skipping automatic stow/go_home"
1496+
" during initial observation. Ensure the robot is already in a"
1497+
" safe pose before continuing.")
1498+
else:
1499+
stow_arm(self._robot)
1500+
go_home(self._robot, self._localizer)
14691501
self._localizer.localize()
14701502
detection_ids = self._detection_id_to_obj.keys()
14711503
detections, vlm_atom_dict = self._run_init_search_for_objects(
14721504
set(detection_ids))
1473-
stow_arm(self._robot)
1505+
if not skip_motion:
1506+
stow_arm(self._robot)
14741507
obj_to_se3_pose = {
14751508
self._detection_id_to_obj[det_id]: val
14761509
for (det_id, val) in detections.items()
@@ -1485,12 +1518,18 @@ def _run_init_search_for_objects(
14851518
"""Have the hand look down from high up at first."""
14861519
assert self._robot is not None
14871520
assert self._localizer is not None
1488-
hand_pose = math_helpers.SE3Pose(x=0.80,
1489-
y=0.0,
1490-
z=0.75,
1491-
rot=math_helpers.Quat.from_pitch(
1492-
np.pi / 3))
1493-
move_hand_to_relative_pose(self._robot, hand_pose)
1521+
skip_motion = (CFG.spot_enable_manual_reset_teleop
1522+
and not CFG.spot_run_dry)
1523+
if skip_motion:
1524+
logging.info("[SpotEnv] Manual reset enabled; keeping operator-set"
1525+
" hand pose for initial scan.")
1526+
else:
1527+
hand_pose = math_helpers.SE3Pose(x=0.80,
1528+
y=0.0,
1529+
z=0.75,
1530+
rot=math_helpers.Quat.from_pitch(
1531+
np.pi / 3))
1532+
move_hand_to_relative_pose(self._robot, hand_pose)
14941533
# Input VLM predicates (to filter task-relevant ones) and objects
14951534
# Obtain detections and additionally VLM ground atoms
14961535
detections, artifacts, vlm_atom_dict = init_search_for_objects(
@@ -5007,7 +5046,7 @@ def _detection_id_to_obj(self) -> Dict[ObjectDetectionID, Object]:
50075046
# NOTE: cup is container type
50085047
objects_to_detect = [
50095048
("cardboard_box", "cardboard_box", _container_type),
5010-
("cup", "orange_cup", _container_type),
5049+
("cup", "green_cup", _container_type),
50115050
]
50125051

50135052
# Add detection object prompt and save object identifier
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Monitor that decides when Spot should run perception updates."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from typing import Tuple
7+
8+
from predicators.settings import CFG
9+
10+
11+
@dataclass(frozen=True)
12+
class PerceptionDecision:
13+
"""Single-step perception request."""
14+
refresh_full: bool
15+
reuse_cache: bool
16+
trigger_pointing: bool
17+
reason: str = ""
18+
19+
@property
20+
def requires_observation(self) -> bool:
21+
return self.refresh_full
22+
23+
24+
class PerceptionMonitor:
25+
"""Small helper that mirrors the execution monitor for perception cadence."""
26+
27+
def __init__(self) -> None:
28+
self._initial_scan_done = True
29+
30+
def reset(self, *, initial_scan_done: bool = True) -> None:
31+
self._initial_scan_done = initial_scan_done
32+
33+
def mark_refresh_complete(self) -> None:
34+
self._initial_scan_done = True
35+
36+
def decide(self, *, has_cached_observation: bool,
37+
is_information_gathering: bool,
38+
is_hand_view_action: bool) -> PerceptionDecision:
39+
"""Compute the perception request for this env step."""
40+
trigger_pointing = is_hand_view_action and CFG.spot_use_vlm_pointing
41+
42+
if not self._initial_scan_done:
43+
return PerceptionDecision(refresh_full=True,
44+
reuse_cache=False,
45+
trigger_pointing=trigger_pointing,
46+
reason="initial perception scan")
47+
48+
if is_hand_view_action:
49+
# Always refresh when we rotate the hand camera to observe a target.
50+
return PerceptionDecision(refresh_full=True,
51+
reuse_cache=False,
52+
trigger_pointing=trigger_pointing,
53+
reason="hand-view observation")
54+
55+
if is_information_gathering:
56+
return PerceptionDecision(refresh_full=True,
57+
reuse_cache=False,
58+
trigger_pointing=trigger_pointing,
59+
reason="information-gathering operator")
60+
61+
if CFG.spot_perception_refresh_observe_only:
62+
if has_cached_observation:
63+
return PerceptionDecision(refresh_full=False,
64+
reuse_cache=True,
65+
trigger_pointing=trigger_pointing,
66+
reason="observe-only reuse")
67+
# No cache available; fall back to a full refresh.
68+
return PerceptionDecision(refresh_full=True,
69+
reuse_cache=False,
70+
trigger_pointing=trigger_pointing,
71+
reason="observe-only but cache empty")
72+
73+
if CFG.spot_skip_perception_for_manipulation and has_cached_observation:
74+
return PerceptionDecision(refresh_full=False,
75+
reuse_cache=True,
76+
trigger_pointing=trigger_pointing,
77+
reason="manipulation skip w/ cache")
78+
79+
return PerceptionDecision(refresh_full=True,
80+
reuse_cache=False,
81+
trigger_pointing=trigger_pointing,
82+
reason="default refresh")

0 commit comments

Comments
 (0)