From 0824ae703faae4fff21fe951772ede35264092b7 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Tue, 23 Apr 2024 17:52:15 -0400 Subject: [PATCH 01/71] try to visualize sokoban - may need nore neat way --- tests/envs/test_sokoban.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/envs/test_sokoban.py b/tests/envs/test_sokoban.py index c0caf0008f..2fd7522133 100644 --- a/tests/envs/test_sokoban.py +++ b/tests/envs/test_sokoban.py @@ -76,6 +76,12 @@ def test_sokoban(): obs = env.reset("test", 1) assert all(np.allclose(m1, m2) for m1, m2 in zip(obs, env_task.init_obs)) imgs = env.render() + + # NOTE: uncomment to visualize env image + # import matplotlib.pyplot as plt + # plt.imshow(imgs[0]) + # plt.show() + assert len(imgs) == 1 task = perceiver.reset(env_task) with pytest.raises(NotImplementedError): From bf4000d3b84643677caabc83dddea5de4fa5e1ba Mon Sep 17 00:00:00 2001 From: Linfeng Date: Tue, 23 Apr 2024 17:53:02 -0400 Subject: [PATCH 02/71] add assert for fast downward planner in running once func --- predicators/planning.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/predicators/planning.py b/predicators/planning.py index 561a434831..38f8c3ae1b 100644 --- a/predicators/planning.py +++ b/predicators/planning.py @@ -1215,6 +1215,9 @@ def run_task_plan_once( raise PlanningFailure( "Skeleton produced by A-star exceeds horizon!") elif "fd" in CFG.sesame_task_planner: # pragma: no cover + # Run Fast Downward. See the instructions in the docstring of `_sesame_plan_with_fast_downward` + assert "FD_EXEC_PATH" in os.environ, \ + "Please follow the instructions in the docstring of this method!" fd_exec_path = os.environ["FD_EXEC_PATH"] exec_str = os.path.join(fd_exec_path, "fast-downward.py") timeout_cmd = "gtimeout" if sys.platform == "darwin" \ From 19d9d53c0eb10d2074533058121b403a436c20dd Mon Sep 17 00:00:00 2001 From: Linfeng Date: Tue, 23 Apr 2024 17:58:02 -0400 Subject: [PATCH 03/71] try to use `rich` package for more structured console output! --- predicators/args.py | 1 + predicators/main.py | 10 ++++++++-- setup.py | 3 ++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/predicators/args.py b/predicators/args.py index 56a085a454..11ba7a1a39 100644 --- a/predicators/args.py +++ b/predicators/args.py @@ -43,6 +43,7 @@ def create_arg_parser(env_required: bool = True, parser.add_argument("--experiment_id", default="", type=str) parser.add_argument("--load_experiment_id", default="", type=str) parser.add_argument("--log_file", default="", type=str) + parser.add_argument("--log_rich", default="true", type=str) parser.add_argument("--use_gui", action="store_true") parser.add_argument('--debug', action="store_const", diff --git a/predicators/main.py b/predicators/main.py index 06e7226a81..0f9b4daada 100644 --- a/predicators/main.py +++ b/predicators/main.py @@ -71,8 +71,14 @@ def main() -> None: args = utils.parse_args() utils.update_config(args) str_args = " ".join(sys.argv) - # Log to stderr. - handlers: List[logging.Handler] = [logging.StreamHandler()] + # Log to stderr or use `rich` package for more structured output. + handlers: List[logging.Handler] = [] + if CFG.log_rich: + from rich.logging import RichHandler + handlers.append(RichHandler()) + else: + handlers.append(logging.StreamHandler()) + if CFG.log_file: handlers.append(logging.FileHandler(CFG.log_file, mode='w')) logging.basicConfig(level=CFG.loglevel, diff --git a/setup.py b/setup.py index 0f06aeceb9..a0f59c8c55 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,8 @@ "opencv-python == 4.7.0.72", "pg3@git+https://github.com/tomsilver/pg3.git", "gym_sokoban@git+https://github.com/Learning-and-Intelligent-Systems/gym-sokoban.git", # pylint: disable=line-too-long - "pbrspot@git+https://github.com/NishanthJKumar/pbrspot.git" + "pbrspot@git+https://github.com/NishanthJKumar/pbrspot.git", + "rich", ], include_package_data=True, extras_require={ From feaa264b9805652f2e66c80fe002909234fae279 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 25 Apr 2024 19:09:25 -0400 Subject: [PATCH 04/71] upload a naive way to store images --- predicators/envs/spot_env.py | 33 ++++++++++++++++-------- predicators/perception/spot_perceiver.py | 14 +++++++++- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 60ab8e33d2..7835a9ca2b 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Callable, ClassVar, Collection, Dict, Iterator, List, \ - Optional, Sequence, Set, Tuple + Optional, Sequence, Set, Tuple, Any import matplotlib import numpy as np @@ -94,6 +94,13 @@ class _PartialPerceptionState(State): in the classifier definitions for the dummy predicates """ + # DEBUG Add an additional field to store Spot images + # This would be directly copied from the images in raw Observation + # NOTE: This is only used when using VLM for predicate evaluation + # NOTE: Performance aspect should be considered later + obs_images: Optional[Dict[str, RGBDImageWithContext]] = None + # TODO: it's still unclear how we select and store useful images! + @property def _simulator_state_predicates(self) -> Set[Predicate]: assert isinstance(self.simulator_state, Dict) @@ -1108,17 +1115,21 @@ def _object_in_xy_classifier(state: State, def _on_classifier(state: State, objects: Sequence[Object]) -> bool: obj_on, obj_surface = objects - # Check that the bottom of the object is close to the top of the surface. - expect = state.get(obj_surface, "z") + state.get(obj_surface, "height") / 2 - actual = state.get(obj_on, "z") - state.get(obj_on, "height") / 2 - classification_val = abs(actual - expect) < _ONTOP_Z_THRESHOLD - - # If so, check that the object is within the bounds of the surface. - if not _object_in_xy_classifier( - state, obj_on, obj_surface, buffer=_ONTOP_SURFACE_BUFFER): - return False + if CFG.spot_vlm_eval_predicate: + print("TODO!!") + print(state.camera_images) + else: + # Check that the bottom of the object is close to the top of the surface. + expect = state.get(obj_surface, "z") + state.get(obj_surface, "height") / 2 + actual = state.get(obj_on, "z") - state.get(obj_on, "height") / 2 + classification_val = abs(actual - expect) < _ONTOP_Z_THRESHOLD + + # If so, check that the object is within the bounds of the surface. + if not _object_in_xy_classifier( + state, obj_on, obj_surface, buffer=_ONTOP_SURFACE_BUFFER): + return False - return classification_val + return classification_val def _top_above_classifier(state: State, objects: Sequence[Object]) -> bool: diff --git a/predicators/perception/spot_perceiver.py b/predicators/perception/spot_perceiver.py index d39107a060..3fc99fe9f9 100644 --- a/predicators/perception/spot_perceiver.py +++ b/predicators/perception/spot_perceiver.py @@ -200,6 +200,12 @@ def _update_state_from_observation(self, observation: Observation) -> None: for obj in observation.objects_in_view: self._lost_objects.discard(obj) + # Add Spot images to the state if needed + # NOTE: This is only used when using VLM for predicate evaluation + # NOTE: Performance aspect should be considered later + if CFG.spot_vlm_eval_predicate: + self._obs_images = observation.images + def _create_state(self) -> State: if self._waiting_for_observation: return DefaultState @@ -281,9 +287,15 @@ def _create_state(self) -> State: # logging.info("Simulator state:") # logging.info(simulator_state) + # Prepare the images from observation + # TODO: we need to strategically add images; now just for test + obs_images = self._obs_images if CFG.spot_vlm_eval_predicate else None + # Now finish the state. state = _PartialPerceptionState(percept_state.data, - simulator_state=simulator_state) + simulator_state=simulator_state, + obs_images=obs_images) + # DEBUG - look into dataclass field init - why warning return state From dd1fbf602a6ed138fcbef04d06153f32f1875013 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 25 Apr 2024 19:10:07 -0400 Subject: [PATCH 05/71] debug --- .../spot_utils/perception/object_detection.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/predicators/spot_utils/perception/object_detection.py b/predicators/spot_utils/perception/object_detection.py index 6757e8f324..17db651917 100644 --- a/predicators/spot_utils/perception/object_detection.py +++ b/predicators/spot_utils/perception/object_detection.py @@ -473,15 +473,17 @@ def get_random_mask_pixel_from_artifacts( mask_idx = rng.choice(len(pixels_in_mask)) pixel_tuple = (pixels_in_mask[1][mask_idx], pixels_in_mask[0][mask_idx]) # Uncomment to plot the grasp pixel being selected! - # rgb_img = artifacts["language"]["rgbds"][camera_name].rgb - # _, axes = plt.subplots() - # axes.imshow(rgb_img) - # axes.add_patch( - # plt.Rectangle((pixel_tuple[0], pixel_tuple[1]), 5, 5, color='red')) - # plt.tight_layout() - # outdir = Path(CFG.spot_perception_outdir) - # plt.savefig(outdir / "grasp_pixel.png", dpi=300) - # plt.close() + """ + rgb_img = artifacts["language"]["rgbds"][camera_name].rgb + _, axes = plt.subplots() + axes.imshow(rgb_img) + axes.add_patch( + plt.Rectangle((pixel_tuple[0], pixel_tuple[1]), 5, 5, color='red')) + plt.tight_layout() + outdir = Path(CFG.spot_perception_outdir) + plt.savefig(outdir / "grasp_pixel.png", dpi=300) + plt.close() + """ return pixel_tuple From 32a06a388cbf060c49f3a0dbe4c35260a684247e Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 25 Apr 2024 19:11:12 -0400 Subject: [PATCH 06/71] upload - manual copy from Nishanth's VLM interface in LIS predicators; we need to figure out a better way to sync later! --- predicators/vlm_interface.py | 166 +++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 predicators/vlm_interface.py diff --git a/predicators/vlm_interface.py b/predicators/vlm_interface.py new file mode 100644 index 0000000000..10e17cae0a --- /dev/null +++ b/predicators/vlm_interface.py @@ -0,0 +1,166 @@ +"""Interface to pretrained vision language models. Takes significant +inspiration from llm_interface.py. + +NOTE: This is manually synced from LIS predicators! + +NOTE: for now, we always assume that images will be appended to the end +of the text prompt. Interleaving text and images is currently not +supported, but should be doable in the future. +""" + +import abc +import logging +import os +import time +from typing import List, Optional + +import openai +import google +import google.generativeai as genai +import imagehash +import PIL.Image + +from predicators.settings import CFG + +# This is a special string that we assume will never appear in a prompt, and +# which we use to separate prompt and completion in the cache. The reason to +# do it this way, rather than saving the prompt and responses separately, +# is that we want it to be easy to browse the cache as text files. +_CACHE_SEP = "\n####$$$###$$$####$$$$###$$$####$$$###$$$###\n" + + +class VisionLanguageModel(abc.ABC): + """A pretrained large language model.""" + + @abc.abstractmethod + def get_id(self) -> str: + """Get a string identifier for this LLM. + + This identifier should include sufficient information so that + querying the same model with the same prompt and same identifier + should yield the same result (assuming temperature 0). + """ + raise NotImplementedError("Override me!") + + @abc.abstractmethod + def _sample_completions(self, + prompt: str, + imgs: List[PIL.Image.Image], + temperature: float, + seed: int, + num_completions: int = 1) -> List[str]: + """This is the main method that subclasses must implement. + + This helper method is called by sample_completions(), which + caches the prompts and responses to disk. + """ + raise NotImplementedError("Override me!") + + def sample_completions(self, + prompt: str, + imgs: List[PIL.Image.Image], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1) -> List[str]: + """Sample one or more completions from a prompt. + + Higher temperatures will increase the variance in the responses. + + The seed may not be used and the results may therefore not be + reproducible for VLMs where we only have access through an API that + does not expose the ability to set a random seed. + + Responses are saved to disk. + """ + # Set up the cache file. + assert _CACHE_SEP not in prompt + os.makedirs(CFG.llm_prompt_cache_dir, exist_ok=True) + vlm_id = self.get_id() + prompt_id = hash(prompt) + # We also need to hash all the images in the prompt. + img_hash_list: List[str] = [] + for img in imgs: + img_hash_list.append(str(imagehash.phash(img))) + imgs_id = "".join(img_hash_list) + # If the temperature is 0, the seed does not matter. + if temperature == 0.0: + config_id = f"most_likely_{num_completions}_{stop_token}" + else: + config_id = f"{temperature}_{seed}_{num_completions}_{stop_token}" + cache_foldername = f"{vlm_id}_{config_id}_{prompt_id}_{imgs_id}" + cache_folderpath = os.path.join(CFG.llm_prompt_cache_dir, + cache_foldername) + os.makedirs(cache_folderpath, exist_ok=True) + cache_filename = "prompt.txt" + cache_filepath = os.path.join(CFG.llm_prompt_cache_dir, + cache_foldername, cache_filename) + if not os.path.exists(cache_filepath): + if CFG.llm_use_cache_only: + raise ValueError("No cached response found for LLM prompt.") + logging.debug(f"Querying VLM {vlm_id} with new prompt.") + # Query the VLM. + completions = self._sample_completions(prompt, imgs, temperature, + seed, num_completions) + # Cache the completion. + cache_str = prompt + _CACHE_SEP + _CACHE_SEP.join(completions) + with open(cache_filepath, 'w', encoding='utf-8') as f: + f.write(cache_str) + # Also save the images for easy debugging. + imgs_folderpath = os.path.join(cache_folderpath, "imgs") + os.makedirs(imgs_folderpath, exist_ok=True) + for i, img in enumerate(imgs): + filename_suffix = str(i) + ".jpg" + img.save(os.path.join(imgs_folderpath, filename_suffix)) + logging.debug(f"Saved VLM response to {cache_filepath}.") + # Load the saved completion. + with open(cache_filepath, 'r', encoding='utf-8') as f: + cache_str = f.read() + logging.debug(f"Loaded VLM response from {cache_filepath}.") + assert cache_str.count(_CACHE_SEP) == num_completions + cached_prompt, completion_strs = cache_str.split(_CACHE_SEP, 1) + assert cached_prompt == prompt + completions = completion_strs.split(_CACHE_SEP) + return completions + + +class GoogleGeminiVLM(VisionLanguageModel): + """Interface to the Google Gemini VLM (1.5). + + Assumes that an environment variable ... + """ + + def __init__(self, model_name: str) -> None: + """See https://ai.google.dev/models/gemini for the list of available + model names.""" + self._model_name = model_name + assert "GOOGLE_API_KEY" in os.environ + genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) + self._model = genai.GenerativeModel(self._model_name) # pylint:disable=no-member + + def get_id(self) -> str: + return f"Google-{self._model_name}" + + def _sample_completions(self, + prompt: str, + imgs: List[PIL.Image.Image], + temperature: float, + seed: int, + num_completions: int = 1) -> List[str]: + del seed # unused + generation_config = genai.types.GenerationConfig( # pylint:disable=no-member + candidate_count=num_completions, + temperature=temperature) + response = None + while response is None: + try: + response = self._model.generate_content( + [prompt] + imgs, generation_config=generation_config) + except google.api_core.exceptions.ResourceExhausted: + # In this case, we've hit a rate limit. Simply wait 3s and + # try again. + logging.debug( + "Hit rate limit for Gemini queries; trying again in 3s!") + time.sleep(3.0) + response.resolve() + return [response.text] From ee3f634ebcc09deb051b5b50130137e630af207e Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 25 Apr 2024 19:12:04 -0400 Subject: [PATCH 07/71] add OpenAI vlm - in progress --- predicators/vlm_interface.py | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/predicators/vlm_interface.py b/predicators/vlm_interface.py index 10e17cae0a..290e3abad4 100644 --- a/predicators/vlm_interface.py +++ b/predicators/vlm_interface.py @@ -164,3 +164,45 @@ def _sample_completions(self, time.sleep(3.0) response.resolve() return [response.text] + + +class OpenAIVLM(VisionLanguageModel): + """Interface to the OpenAI VLM.""" + + def __init__(self, model_name: str) -> None: + self._model_name = model_name + assert "OPENAI_API_KEY" in os.environ + openai.api_key = os.getenv("OPENAI_API_KEY") + + def get_id(self) -> str: + return f"OpenAI-{self._model_name}" + + def _sample_completions(self, + prompt: str, + imgs: List[PIL.Image.Image], + temperature: float, + seed: int, + num_completions: int = 1) -> List[str]: + # TODO run and test + response = openai.Completion.create( + model=self._model_name, + prompt=prompt, + images=[image.tobytes() for image in imgs], + temperature=temperature, + max_tokens=2048, + n=num_completions, + stop=None, + seed=seed) + return [completion.choices[0].text for completion in response.choices] + + +if __name__ == '__main__': + vlm_list = [OpenAIVLM, GoogleGeminiVLM] + vlm_class = vlm_list[1] + + # TODO test + vlm = vlm_class("text-to-image") + prompt = "A beautiful sunset over a lake." + imgs = [PIL.Image.open("sunset.jpg")] + completions = vlm.sample_completions(prompt, imgs, temperature=0.0, seed=0) + print(completions) From a1a67fd20b8e62dd0c0da6b3a2117e10068bb701 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 25 Apr 2024 19:59:11 -0400 Subject: [PATCH 08/71] update config setting for using vlm --- predicators/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/predicators/settings.py b/predicators/settings.py index f3957cf75d..89c7f606c4 100644 --- a/predicators/settings.py +++ b/predicators/settings.py @@ -46,6 +46,9 @@ class GlobalSettings: # your call to utils.reset_config(). render_state_dpi = 150 approach_wrapper = None + # Use VLMs to evaluate some spatial predicates in visual environment, + # e.g., Sokoban. Still work in progress. + enable_vlm_eval_predicate = False # cover_multistep_options env parameters cover_multistep_action_limits = [-np.inf, np.inf] @@ -178,6 +181,8 @@ class GlobalSettings: spot_run_dry = False spot_use_perfect_samplers = False # for debugging spot_sweep_env_goal_description = "get the objects into the bucket" + # Evaluate some predicates with VLM; need additional setup; WIP + spot_vlm_eval_predicate = False # pddl blocks env parameters pddl_blocks_procedural_train_min_num_blocks = 3 From 5d9f12ef8376e7965f1d2a8df6005d8c9b504377 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 25 Apr 2024 20:00:05 -0400 Subject: [PATCH 09/71] add package --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a0f59c8c55..d19d11c60c 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ "pg3@git+https://github.com/tomsilver/pg3.git", "gym_sokoban@git+https://github.com/Learning-and-Intelligent-Systems/gym-sokoban.git", # pylint: disable=line-too-long "pbrspot@git+https://github.com/NishanthJKumar/pbrspot.git", + "google-generativeai", "rich", ], include_package_data=True, From cf3dbf79685eb7a2949a7f6d102ccb77da5b2a93 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 25 Apr 2024 20:02:08 -0400 Subject: [PATCH 10/71] another missed one --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d19d11c60c..d250ace479 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ "gym_sokoban@git+https://github.com/Learning-and-Intelligent-Systems/gym-sokoban.git", # pylint: disable=line-too-long "pbrspot@git+https://github.com/NishanthJKumar/pbrspot.git", "google-generativeai", + "imagehash", "rich", ], include_package_data=True, From 3001f32d2cfad7da471c85b1372edbbe7c0a161b Mon Sep 17 00:00:00 2001 From: Linfeng Date: Mon, 29 Apr 2024 17:02:26 -0400 Subject: [PATCH 11/71] manually add Nishanth's new pretrained model interface for now, see LIS predicators! --- predicators/pretrained_model_interface.py | 251 ++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 predicators/pretrained_model_interface.py diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py new file mode 100644 index 0000000000..8bee546259 --- /dev/null +++ b/predicators/pretrained_model_interface.py @@ -0,0 +1,251 @@ +"""Interface to pretrained large models. + +These might be joint Vision-Language Models (VLM's) or Large Language +Models (LLM's) +""" + +import abc +import logging +import os +import time +from typing import List, Optional + +import google +import google.generativeai as genai +import imagehash +import openai +import PIL.Image + +from predicators.settings import CFG + +# This is a special string that we assume will never appear in a prompt, and +# which we use to separate prompt and completion in the cache. The reason to +# do it this way, rather than saving the prompt and responses separately, +# is that we want it to be easy to browse the cache as text files. +_CACHE_SEP = "\n####$$$###$$$####$$$$###$$$####$$$###$$$###\n" + + +class PretrainedLargeModel(abc.ABC): + """A pretrained large vision or language model.""" + + @abc.abstractmethod + def get_id(self) -> str: + """Get a string identifier for this model. + + This identifier should include sufficient information so that + querying the same model with the same prompt and same identifier + should yield the same result (assuming temperature 0). + """ + raise NotImplementedError("Override me!") + + @abc.abstractmethod + def _sample_completions(self, + prompt: str, + imgs: Optional[List[PIL.Image.Image]], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1) -> List[str]: + """This is the main method that subclasses must implement. + + This helper method is called by sample_completions(), which + caches the prompts and responses to disk. + """ + raise NotImplementedError("Override me!") + + def sample_completions(self, + prompt: str, + imgs: Optional[List[PIL.Image.Image]], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1) -> List[str]: + """Sample one or more completions from a prompt. + + Higher temperatures will increase the variance in the responses. + The seed may not be used and the results may therefore not be + reproducible for models where we only have access through an API + that does not expose the ability to set a random seed. Responses + are saved to disk. + """ + # Set up the cache file. + assert _CACHE_SEP not in prompt + os.makedirs(CFG.pretrained_model_prompt_cache_dir, exist_ok=True) + model_id = self.get_id() + prompt_id = hash(prompt) + config_id = f"{temperature}_{seed}_{num_completions}_" + \ + f"{stop_token}" + # If the temperature is 0, the seed does not matter. + if temperature == 0.0: + config_id = f"most_likely_{num_completions}_{stop_token}" + cache_foldername = f"{model_id}_{config_id}_{prompt_id}" + if imgs is not None: + # We also need to hash all the images in the prompt. + img_hash_list: List[str] = [] + for img in imgs: + img_hash_list.append(str(imagehash.phash(img))) + # NOTE: it's very possible that this string gets too long and this + # causes significant problems for us. We can fix this when it + # comes up by hashing this string to a shorter string, using e.g. + # https://stackoverflow.com/questions/57263436/hash-like-string-shortener-with-decoder # pylint:disable=line-too-long + imgs_id = "".join(img_hash_list) + cache_foldername += f"{imgs_id}" + cache_folderpath = os.path.join(CFG.pretrained_model_prompt_cache_dir, + cache_foldername) + os.makedirs(cache_folderpath, exist_ok=True) + cache_filename = "prompt.txt" + cache_filepath = os.path.join(CFG.pretrained_model_prompt_cache_dir, + cache_foldername, cache_filename) + if not os.path.exists(cache_filepath): + if CFG.llm_use_cache_only: + raise ValueError("No cached response found for prompt.") + logging.debug(f"Querying model {model_id} with new prompt.") + # Query the model. + completions = self._sample_completions(prompt, imgs, temperature, + seed, stop_token, + num_completions) + # Cache the completion. + cache_str = prompt + _CACHE_SEP + _CACHE_SEP.join(completions) + with open(cache_filepath, 'w', encoding='utf-8') as f: + f.write(cache_str) + if imgs is not None: + # Also save the images for easy debugging. + imgs_folderpath = os.path.join(cache_folderpath, "imgs") + os.makedirs(imgs_folderpath, exist_ok=True) + for i, img in enumerate(imgs): + filename_suffix = str(i) + ".jpg" + img.save(os.path.join(imgs_folderpath, filename_suffix)) + logging.debug(f"Saved model response to {cache_filepath}.") + # Load the saved completion. + with open(cache_filepath, 'r', encoding='utf-8') as f: + cache_str = f.read() + logging.debug(f"Loaded model response from {cache_filepath}.") + assert cache_str.count(_CACHE_SEP) == num_completions + cached_prompt, completion_strs = cache_str.split(_CACHE_SEP, 1) + assert cached_prompt == prompt + completions = completion_strs.split(_CACHE_SEP) + return completions + + +class VisionLanguageModel(PretrainedLargeModel): + """A class for all VLM's.""" + + def sample_completions( + self, + prompt: str, + imgs: Optional[List[PIL.Image.Image]], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1) -> List[str]: # pragma: no cover + assert imgs is not None + return super().sample_completions(prompt, imgs, temperature, seed, + stop_token, num_completions) + + +class LargeLanguageModel(PretrainedLargeModel): + """A class for all LLM's.""" + + def sample_completions( + self, + prompt: str, + imgs: Optional[List[PIL.Image.Image]], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1) -> List[str]: # pragma: no cover + assert imgs is None + return super().sample_completions(prompt, imgs, temperature, seed, + stop_token, num_completions) + + +class OpenAILLM(LargeLanguageModel): + """Interface to openAI LLMs (GPT-3). + + Assumes that an environment variable OPENAI_API_KEY is set to a + private API key for beta.openai.com. + """ + + def __init__(self, model_name: str) -> None: + """See https://beta.openai.com/docs/models/gpt-3 for the list of + available model names.""" + self._model_name = model_name + # Note that max_tokens is the maximum response length (not prompt). + # From OpenAI docs: "The token count of your prompt plus max_tokens + # cannot exceed the model's context length." + self._max_tokens = CFG.llm_openai_max_response_tokens + assert "OPENAI_API_KEY" in os.environ + openai.api_key = os.getenv("OPENAI_API_KEY") + + def get_id(self) -> str: + return f"openai-{self._model_name}" + + def _sample_completions( + self, + prompt: str, + imgs: Optional[List[PIL.Image.Image]], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1) -> List[str]: # pragma: no cover + del imgs, seed # unused + response = openai.Completion.create( + model=self._model_name, # type: ignore + prompt=prompt, + temperature=temperature, + max_tokens=self._max_tokens, + stop=stop_token, + n=num_completions) + assert len(response["choices"]) == num_completions + text_responses = [ + response["choices"][i]["text"] for i in range(num_completions) + ] + return text_responses + + +class GoogleGeminiVLM(VisionLanguageModel): + """Interface to the Google Gemini VLM (1.5). + + Assumes that an environment variable GOOGLE_API_KEY is set with the + necessary API key to query the particular model name. + """ + + def __init__(self, model_name: str) -> None: + """See https://ai.google.dev/models/gemini for the list of available + model names.""" + self._model_name = model_name + assert "GOOGLE_API_KEY" in os.environ + genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) + self._model = genai.GenerativeModel(self._model_name) # pylint:disable=no-member + + def get_id(self) -> str: + return f"Google-{self._model_name}" + + def _sample_completions( + self, + prompt: str, + imgs: Optional[List[PIL.Image.Image]], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1) -> List[str]: # pragma: no cover + del seed, stop_token # unused + assert imgs is not None + generation_config = genai.types.GenerationConfig( # pylint:disable=no-member + candidate_count=num_completions, + temperature=temperature) + response = None + while response is None: + try: + response = self._model.generate_content( + [prompt] + imgs, + generation_config=generation_config) # type: ignore + break + except google.api_core.exceptions.ResourceExhausted: + # In this case, we've hit a rate limit. Simply wait 3s and + # try again. + logging.debug( + "Hit rate limit for Gemini queries; trying again in 3s!") + time.sleep(3.0) + response.resolve() + return [response.text] From 4d47211be771e5f6e691512bf45d00d8ad5c433f Mon Sep 17 00:00:00 2001 From: Linfeng Date: Mon, 29 Apr 2024 19:30:15 -0400 Subject: [PATCH 12/71] add new OpenAI VLM class, add example to use --- predicators/pretrained_model_interface.py | 123 +++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py index 8bee546259..9b92f37d95 100644 --- a/predicators/pretrained_model_interface.py +++ b/predicators/pretrained_model_interface.py @@ -5,17 +5,22 @@ """ import abc +import base64 import logging import os import time +from io import BytesIO from typing import List, Optional +import cv2 import google import google.generativeai as genai import imagehash import openai import PIL.Image +from tenacity import retry, stop_after_attempt, wait_random_exponential +from predicators import utils from predicators.settings import CFG # This is a special string that we assume will never appear in a prompt, and @@ -74,7 +79,7 @@ def sample_completions(self, model_id = self.get_id() prompt_id = hash(prompt) config_id = f"{temperature}_{seed}_{num_completions}_" + \ - f"{stop_token}" + f"{stop_token}" # If the temperature is 0, the seed does not matter. if temperature == 0.0: config_id = f"most_likely_{num_completions}_{stop_token}" @@ -249,3 +254,119 @@ def _sample_completions( time.sleep(3.0) response.resolve() return [response.text] + + +class OpenAIVLM(VisionLanguageModel): + """Interface for OpenAI's VLMs, including GPT-4 Turbo (and preview versions).""" + + def __init__(self, model_name: str): + """Initialize with a specific model name.""" + self.model_name = model_name + self.set_openai_key() + + def set_openai_key(self, key: Optional[str] = None): + """Set the OpenAI API key.""" + if key is None: + assert "OPENAI_API_KEY" in os.environ + key = os.environ["OPENAI_API_KEY"] + openai.api_key = key + + def prepare_openai_messages(self, content: str): + """Prepare text-only messages for the OpenAI API.""" + return [{"role": "user", "content": content}] + + def prepare_openai_vision_messages( + self, prefix: Optional[str] = None, suffix: Optional[str] = None, + image_paths: Optional[List[str]] = None, image_size: Optional[int] = 512): + """Prepare text and image messages for the OpenAI API.""" + if image_paths is None: + image_paths = [] + elif not isinstance(image_paths, list): + image_paths = [image_paths] + + content = [] + + if prefix: + content.append({"text": prefix, "type": "text"}) + + for path in image_paths: + if not isinstance(path, str): + print(f"Invalid image path: {path}") + continue + if not os.path.exists(path): + print(f"Image file not found: {path}") + continue + + frame = cv2.imread(path) + if image_size: + factor = image_size / max(frame.shape[:2]) + frame = cv2.resize(frame, dsize=None, fx=factor, fy=factor) + _, buffer = cv2.imencode(".png", frame) + frame = base64.b64encode(buffer).decode("utf-8") + content.append({"image_url": {"url": f"data:image/png:base64,{frame}"}, "type": "image_url"}) + + if suffix: + content.append({"text": suffix, "type": "text"}) + + return [{"role": "user", "content": content}] + + @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)) + def call_openai_api(self, messages: list, model: str = "gpt-4", seed: Optional[int] = None, max_tokens: int = 32, + temperature: float = 0.2, verbose: bool = False): + """Make an API call to OpenAI.""" + client = openai.OpenAI() + completion = client.chat.completions.create( + model=model, + messages=messages, + seed=seed, + max_tokens=max_tokens, + temperature=temperature, + ) + if verbose: + print(f"OpenAI API response: {completion}") + assert len(completion.choices) == 1 + return completion.choices[0].message.content + + def get_id(self) -> str: + """Get an identifier for the model.""" + return f"OpenAI-{self.model_name}" + + def _sample_completions(self, + prompt: str, + imgs: Optional[List[PIL.Image.Image]], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1) -> List[str]: + """Query the model and get responses.""" + messages = self.prepare_openai_messages(prompt) + responses = [ + self.call_openai_api(messages, model=self.model_name, max_tokens=512, temperature=temperature) + for _ in range(num_completions) + ] + return responses + + +# Example usage: +if __name__ == "__main__": + # Make sure the OPENAI_API_KEY is set + model_name = "gpt-4-turbo" + vlm = OpenAIVLM(model_name) + + prompt = """ + Describe the object relationships between the objects and containers. + You can use following predicate-style descriptions: + Inside(object1, container) + Blocking(object1, object2) + On(object, surface) + """ + images = [PIL.Image.open("../test_vlm_predicate_img.jpg")] + + print("Start requesting...") + completions = vlm._sample_completions( + prompt=prompt, + imgs=images, + temperature=0.5, num_completions=1, seed=0 + ) + for i, completion in enumerate(completions): + print(f"Completion {i + 1}: {completion}") From e45a0e935c4f7e33ac00f16da4198a0b509e652b Mon Sep 17 00:00:00 2001 From: Linfeng Date: Mon, 29 Apr 2024 19:30:26 -0400 Subject: [PATCH 13/71] add a flag for caching --- predicators/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/predicators/settings.py b/predicators/settings.py index 89c7f606c4..3f903716f9 100644 --- a/predicators/settings.py +++ b/predicators/settings.py @@ -405,7 +405,7 @@ class GlobalSettings: nsrt_rl_valid_reward_steps_threshold = 10 # parameters for large language models - llm_prompt_cache_dir = "llm_cache" + pretrained_model_prompt_cache_dir = "pretrained_model_cache" llm_openai_max_response_tokens = 700 llm_use_cache_only = False llm_model_name = "text-curie-001" # "text-davinci-002" From 8786dcd94ca02be7ee986b518842a0aedb726672 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Mon, 29 Apr 2024 19:45:20 -0400 Subject: [PATCH 14/71] now the example working - fix requesting vision messages, update test --- predicators/pretrained_model_interface.py | 49 +++++++++-------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py index 9b92f37d95..72b9f124d4 100644 --- a/predicators/pretrained_model_interface.py +++ b/predicators/pretrained_model_interface.py @@ -20,7 +20,6 @@ import PIL.Image from tenacity import retry, stop_after_attempt, wait_random_exponential -from predicators import utils from predicators.settings import CFG # This is a special string that we assume will never appear in a prompt, and @@ -271,39 +270,29 @@ def set_openai_key(self, key: Optional[str] = None): key = os.environ["OPENAI_API_KEY"] openai.api_key = key - def prepare_openai_messages(self, content: str): - """Prepare text-only messages for the OpenAI API.""" - return [{"role": "user", "content": content}] - - def prepare_openai_vision_messages( - self, prefix: Optional[str] = None, suffix: Optional[str] = None, - image_paths: Optional[List[str]] = None, image_size: Optional[int] = 512): + def prepare_vision_messages( + self, images: List[PIL.Image.Image], + prefix: Optional[str] = None, suffix: Optional[str] = None, image_size: Optional[int] = 512): """Prepare text and image messages for the OpenAI API.""" - if image_paths is None: - image_paths = [] - elif not isinstance(image_paths, list): - image_paths = [image_paths] - content = [] if prefix: content.append({"text": prefix, "type": "text"}) - for path in image_paths: - if not isinstance(path, str): - print(f"Invalid image path: {path}") - continue - if not os.path.exists(path): - print(f"Image file not found: {path}") - continue - - frame = cv2.imread(path) + assert images + for img in images: + img_resized = img if image_size: - factor = image_size / max(frame.shape[:2]) - frame = cv2.resize(frame, dsize=None, fx=factor, fy=factor) - _, buffer = cv2.imencode(".png", frame) + factor = image_size / max(img.size) + img_resized = img.resize((int(img.size[0] * factor), int(img.size[1] * factor))) + + # Convert the image to PNG format and encode it in base64 + buffer = BytesIO() + img_resized.save(buffer, format="PNG") + buffer = buffer.getvalue() frame = base64.b64encode(buffer).decode("utf-8") - content.append({"image_url": {"url": f"data:image/png:base64,{frame}"}, "type": "image_url"}) + + content.append({"image_url": {"url": f"data:image/png;base64,{frame}"}, "type": "image_url"}) if suffix: content.append({"text": suffix, "type": "text"}) @@ -339,7 +328,7 @@ def _sample_completions(self, stop_token: Optional[str] = None, num_completions: int = 1) -> List[str]: """Query the model and get responses.""" - messages = self.prepare_openai_messages(prompt) + messages = self.prepare_vision_messages(prefix=prompt, images=imgs) responses = [ self.call_openai_api(messages, model=self.model_name, max_tokens=512, temperature=temperature) for _ in range(num_completions) @@ -363,10 +352,10 @@ def _sample_completions(self, images = [PIL.Image.open("../test_vlm_predicate_img.jpg")] print("Start requesting...") - completions = vlm._sample_completions( + completions = vlm.sample_completions( prompt=prompt, imgs=images, - temperature=0.5, num_completions=1, seed=0 + temperature=0.5, num_completions=3, seed=0 ) for i, completion in enumerate(completions): - print(f"Completion {i + 1}: {completion}") + print(f"Completion {i + 1}: \n{completion}\n") From 71d0b3e06e38284d7243c6e28884be1f6fbb2b91 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Mon, 29 Apr 2024 19:46:39 -0400 Subject: [PATCH 15/71] update; add choosing img detail quality --- predicators/pretrained_model_interface.py | 34 +++++++++++++++-------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py index 72b9f124d4..3c359389bb 100644 --- a/predicators/pretrained_model_interface.py +++ b/predicators/pretrained_model_interface.py @@ -272,7 +272,9 @@ def set_openai_key(self, key: Optional[str] = None): def prepare_vision_messages( self, images: List[PIL.Image.Image], - prefix: Optional[str] = None, suffix: Optional[str] = None, image_size: Optional[int] = 512): + prefix: Optional[str] = None, suffix: Optional[str] = None, image_size: Optional[int] = 512, + detail: str = "auto" + ): """Prepare text and image messages for the OpenAI API.""" content = [] @@ -280,6 +282,7 @@ def prepare_vision_messages( content.append({"text": prefix, "type": "text"}) assert images + assert detail in ["auto", "low", "high"] for img in images: img_resized = img if image_size: @@ -292,7 +295,13 @@ def prepare_vision_messages( buffer = buffer.getvalue() frame = base64.b64encode(buffer).decode("utf-8") - content.append({"image_url": {"url": f"data:image/png;base64,{frame}"}, "type": "image_url"}) + content.append({ + "image_url": { + "url": f"data:image/png;base64,{frame}", + "detail": "auto" + }, + "type": "image_url" + }) if suffix: content.append({"text": suffix, "type": "text"}) @@ -320,17 +329,20 @@ def get_id(self) -> str: """Get an identifier for the model.""" return f"OpenAI-{self.model_name}" - def _sample_completions(self, - prompt: str, - imgs: Optional[List[PIL.Image.Image]], - temperature: float, - seed: int, - stop_token: Optional[str] = None, - num_completions: int = 1) -> List[str]: + def _sample_completions( + self, + prompt: str, + imgs: Optional[List[PIL.Image.Image]], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1, + max_tokens=512, + ) -> List[str]: """Query the model and get responses.""" - messages = self.prepare_vision_messages(prefix=prompt, images=imgs) + messages = self.prepare_vision_messages(prefix=prompt, images=imgs, detail="auto") responses = [ - self.call_openai_api(messages, model=self.model_name, max_tokens=512, temperature=temperature) + self.call_openai_api(messages, model=self.model_name, max_tokens=max_tokens, temperature=temperature) for _ in range(num_completions) ] return responses From 112b690ceea830f291fcee143dfb021d1a3883bf Mon Sep 17 00:00:00 2001 From: Linfeng Date: Mon, 29 Apr 2024 19:49:58 -0400 Subject: [PATCH 16/71] include the test image I used for now, not sure what I should do with this image --- test_vlm_predicate_img.jpg | Bin 0 -> 209620 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test_vlm_predicate_img.jpg diff --git a/test_vlm_predicate_img.jpg b/test_vlm_predicate_img.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2c3550448300c86ba6b931a0b29b5e3282f34161 GIT binary patch literal 209620 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 z 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 E9}jXKW&i*H literal 0 HcmV?d00001 From f884df7dffc556d90dd3ec65d7eafe7f82f59f62 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Mon, 29 Apr 2024 19:50:37 -0400 Subject: [PATCH 17/71] remove original vlm interface, already merged into latest pretrained py one --- predicators/vlm_interface.py | 208 ----------------------------------- 1 file changed, 208 deletions(-) delete mode 100644 predicators/vlm_interface.py diff --git a/predicators/vlm_interface.py b/predicators/vlm_interface.py deleted file mode 100644 index 290e3abad4..0000000000 --- a/predicators/vlm_interface.py +++ /dev/null @@ -1,208 +0,0 @@ -"""Interface to pretrained vision language models. Takes significant -inspiration from llm_interface.py. - -NOTE: This is manually synced from LIS predicators! - -NOTE: for now, we always assume that images will be appended to the end -of the text prompt. Interleaving text and images is currently not -supported, but should be doable in the future. -""" - -import abc -import logging -import os -import time -from typing import List, Optional - -import openai -import google -import google.generativeai as genai -import imagehash -import PIL.Image - -from predicators.settings import CFG - -# This is a special string that we assume will never appear in a prompt, and -# which we use to separate prompt and completion in the cache. The reason to -# do it this way, rather than saving the prompt and responses separately, -# is that we want it to be easy to browse the cache as text files. -_CACHE_SEP = "\n####$$$###$$$####$$$$###$$$####$$$###$$$###\n" - - -class VisionLanguageModel(abc.ABC): - """A pretrained large language model.""" - - @abc.abstractmethod - def get_id(self) -> str: - """Get a string identifier for this LLM. - - This identifier should include sufficient information so that - querying the same model with the same prompt and same identifier - should yield the same result (assuming temperature 0). - """ - raise NotImplementedError("Override me!") - - @abc.abstractmethod - def _sample_completions(self, - prompt: str, - imgs: List[PIL.Image.Image], - temperature: float, - seed: int, - num_completions: int = 1) -> List[str]: - """This is the main method that subclasses must implement. - - This helper method is called by sample_completions(), which - caches the prompts and responses to disk. - """ - raise NotImplementedError("Override me!") - - def sample_completions(self, - prompt: str, - imgs: List[PIL.Image.Image], - temperature: float, - seed: int, - stop_token: Optional[str] = None, - num_completions: int = 1) -> List[str]: - """Sample one or more completions from a prompt. - - Higher temperatures will increase the variance in the responses. - - The seed may not be used and the results may therefore not be - reproducible for VLMs where we only have access through an API that - does not expose the ability to set a random seed. - - Responses are saved to disk. - """ - # Set up the cache file. - assert _CACHE_SEP not in prompt - os.makedirs(CFG.llm_prompt_cache_dir, exist_ok=True) - vlm_id = self.get_id() - prompt_id = hash(prompt) - # We also need to hash all the images in the prompt. - img_hash_list: List[str] = [] - for img in imgs: - img_hash_list.append(str(imagehash.phash(img))) - imgs_id = "".join(img_hash_list) - # If the temperature is 0, the seed does not matter. - if temperature == 0.0: - config_id = f"most_likely_{num_completions}_{stop_token}" - else: - config_id = f"{temperature}_{seed}_{num_completions}_{stop_token}" - cache_foldername = f"{vlm_id}_{config_id}_{prompt_id}_{imgs_id}" - cache_folderpath = os.path.join(CFG.llm_prompt_cache_dir, - cache_foldername) - os.makedirs(cache_folderpath, exist_ok=True) - cache_filename = "prompt.txt" - cache_filepath = os.path.join(CFG.llm_prompt_cache_dir, - cache_foldername, cache_filename) - if not os.path.exists(cache_filepath): - if CFG.llm_use_cache_only: - raise ValueError("No cached response found for LLM prompt.") - logging.debug(f"Querying VLM {vlm_id} with new prompt.") - # Query the VLM. - completions = self._sample_completions(prompt, imgs, temperature, - seed, num_completions) - # Cache the completion. - cache_str = prompt + _CACHE_SEP + _CACHE_SEP.join(completions) - with open(cache_filepath, 'w', encoding='utf-8') as f: - f.write(cache_str) - # Also save the images for easy debugging. - imgs_folderpath = os.path.join(cache_folderpath, "imgs") - os.makedirs(imgs_folderpath, exist_ok=True) - for i, img in enumerate(imgs): - filename_suffix = str(i) + ".jpg" - img.save(os.path.join(imgs_folderpath, filename_suffix)) - logging.debug(f"Saved VLM response to {cache_filepath}.") - # Load the saved completion. - with open(cache_filepath, 'r', encoding='utf-8') as f: - cache_str = f.read() - logging.debug(f"Loaded VLM response from {cache_filepath}.") - assert cache_str.count(_CACHE_SEP) == num_completions - cached_prompt, completion_strs = cache_str.split(_CACHE_SEP, 1) - assert cached_prompt == prompt - completions = completion_strs.split(_CACHE_SEP) - return completions - - -class GoogleGeminiVLM(VisionLanguageModel): - """Interface to the Google Gemini VLM (1.5). - - Assumes that an environment variable ... - """ - - def __init__(self, model_name: str) -> None: - """See https://ai.google.dev/models/gemini for the list of available - model names.""" - self._model_name = model_name - assert "GOOGLE_API_KEY" in os.environ - genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) - self._model = genai.GenerativeModel(self._model_name) # pylint:disable=no-member - - def get_id(self) -> str: - return f"Google-{self._model_name}" - - def _sample_completions(self, - prompt: str, - imgs: List[PIL.Image.Image], - temperature: float, - seed: int, - num_completions: int = 1) -> List[str]: - del seed # unused - generation_config = genai.types.GenerationConfig( # pylint:disable=no-member - candidate_count=num_completions, - temperature=temperature) - response = None - while response is None: - try: - response = self._model.generate_content( - [prompt] + imgs, generation_config=generation_config) - except google.api_core.exceptions.ResourceExhausted: - # In this case, we've hit a rate limit. Simply wait 3s and - # try again. - logging.debug( - "Hit rate limit for Gemini queries; trying again in 3s!") - time.sleep(3.0) - response.resolve() - return [response.text] - - -class OpenAIVLM(VisionLanguageModel): - """Interface to the OpenAI VLM.""" - - def __init__(self, model_name: str) -> None: - self._model_name = model_name - assert "OPENAI_API_KEY" in os.environ - openai.api_key = os.getenv("OPENAI_API_KEY") - - def get_id(self) -> str: - return f"OpenAI-{self._model_name}" - - def _sample_completions(self, - prompt: str, - imgs: List[PIL.Image.Image], - temperature: float, - seed: int, - num_completions: int = 1) -> List[str]: - # TODO run and test - response = openai.Completion.create( - model=self._model_name, - prompt=prompt, - images=[image.tobytes() for image in imgs], - temperature=temperature, - max_tokens=2048, - n=num_completions, - stop=None, - seed=seed) - return [completion.choices[0].text for completion in response.choices] - - -if __name__ == '__main__': - vlm_list = [OpenAIVLM, GoogleGeminiVLM] - vlm_class = vlm_list[1] - - # TODO test - vlm = vlm_class("text-to-image") - prompt = "A beautiful sunset over a lake." - imgs = [PIL.Image.open("sunset.jpg")] - completions = vlm.sample_completions(prompt, imgs, temperature=0.0, seed=0) - print(completions) From aec70de010ed6dcf894449101d024c4a4d57486d Mon Sep 17 00:00:00 2001 From: Linfeng Date: Tue, 30 Apr 2024 18:54:30 -0400 Subject: [PATCH 18/71] found a way to use VLM to evaluate; add current images and also visible objects --- predicators/perception/spot_perceiver.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/predicators/perception/spot_perceiver.py b/predicators/perception/spot_perceiver.py index 3fc99fe9f9..814ab04193 100644 --- a/predicators/perception/spot_perceiver.py +++ b/predicators/perception/spot_perceiver.py @@ -200,11 +200,11 @@ def _update_state_from_observation(self, observation: Observation) -> None: for obj in observation.objects_in_view: self._lost_objects.discard(obj) - # Add Spot images to the state if needed # NOTE: This is only used when using VLM for predicate evaluation # NOTE: Performance aspect should be considered later if CFG.spot_vlm_eval_predicate: - self._obs_images = observation.images + # Add current Spot images to the state if needed + self._camera_images = observation.images def _create_state(self) -> State: if self._waiting_for_observation: @@ -287,14 +287,14 @@ def _create_state(self) -> State: # logging.info("Simulator state:") # logging.info(simulator_state) - # Prepare the images from observation - # TODO: we need to strategically add images; now just for test - obs_images = self._obs_images if CFG.spot_vlm_eval_predicate else None + # Prepare the current images from observation + camera_images = self._camera_images if CFG.spot_vlm_eval_predicate else None # Now finish the state. state = _PartialPerceptionState(percept_state.data, simulator_state=simulator_state, - obs_images=obs_images) + camera_images=camera_images, + visible_objects=self._objects_in_view) # DEBUG - look into dataclass field init - why warning return state From 94e6a4c1dfd6680e5e383e5ee275a2bbf6f9582b Mon Sep 17 00:00:00 2001 From: Linfeng Date: Tue, 30 Apr 2024 18:58:02 -0400 Subject: [PATCH 19/71] found a way to use VLM to evaluate; check if visible in current scene, only update these predicates --- predicators/envs/spot_env.py | 78 ++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 7835a9ca2b..20fb77a8a1 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -94,12 +94,12 @@ class _PartialPerceptionState(State): in the classifier definitions for the dummy predicates """ - # DEBUG Add an additional field to store Spot images - # This would be directly copied from the images in raw Observation - # NOTE: This is only used when using VLM for predicate evaluation - # NOTE: Performance aspect should be considered later - obs_images: Optional[Dict[str, RGBDImageWithContext]] = None - # TODO: it's still unclear how we select and store useful images! + # # DEBUG Add an additional field to store Spot images + # # This would be directly copied from the images in raw Observation + # # NOTE: This is only used when using VLM for predicate evaluation + # # NOTE: Performance aspect should be considered later + # cam_images: Optional[Dict[str, RGBDImageWithContext]] = None + # # TODO: it's still unclear how we select and store useful images! @property def _simulator_state_predicates(self) -> Set[Predicate]: @@ -128,7 +128,8 @@ def copy(self) -> State: "atoms": self._simulator_state_atoms.copy() } return _PartialPerceptionState(state_copy, - simulator_state=sim_state_copy) + simulator_state=sim_state_copy, + camera_images=self.camera_images) def _create_dummy_predicate_classifier( @@ -1114,10 +1115,16 @@ def _object_in_xy_classifier(state: State, def _on_classifier(state: State, objects: Sequence[Object]) -> bool: obj_on, obj_surface = objects + currently_visible = all([o in state.visible_objects for o in objects]) - if CFG.spot_vlm_eval_predicate: - print("TODO!!") - print(state.camera_images) + print(currently_visible, state) + + if CFG.spot_vlm_eval_predicate and not currently_visible: + # TODO: add all previous atoms to the state + raise NotImplementedError + elif CFG.spot_vlm_eval_predicate and currently_visible: + # TODO call VLM to evaluate predicate value + raise NotImplementedError else: # Check that the bottom of the object is close to the top of the surface. expect = state.get(obj_surface, "z") + state.get(obj_surface, "height") / 2 @@ -1143,27 +1150,39 @@ def _top_above_classifier(state: State, objects: Sequence[Object]) -> bool: def _inside_classifier(state: State, objects: Sequence[Object]) -> bool: obj_in, obj_container = objects + currently_visible = all([o in state.visible_objects for o in objects]) - if not _object_in_xy_classifier( - state, obj_in, obj_container, buffer=_INSIDE_SURFACE_BUFFER): - return False + print(currently_visible, state) - obj_z = state.get(obj_in, "z") - obj_half_height = state.get(obj_in, "height") / 2 - obj_bottom = obj_z - obj_half_height - obj_top = obj_z + obj_half_height + if CFG.spot_vlm_eval_predicate and not currently_visible: + # TODO: add all previous atoms to the state + raise NotImplementedError + elif CFG.spot_vlm_eval_predicate and currently_visible: + # TODO call VLM to evaluate predicate value + raise NotImplementedError - container_z = state.get(obj_container, "z") - container_half_height = state.get(obj_container, "height") / 2 - container_bottom = container_z - container_half_height - container_top = container_z + container_half_height + else: - # Check that the bottom is "above" the bottom of the container. - if obj_bottom < container_bottom - _INSIDE_Z_THRESHOLD: - return False + if not _object_in_xy_classifier( + state, obj_in, obj_container, buffer=_INSIDE_SURFACE_BUFFER): + return False - # Check that the top is "below" the top of the container. - return obj_top < container_top + _INSIDE_Z_THRESHOLD + obj_z = state.get(obj_in, "z") + obj_half_height = state.get(obj_in, "height") / 2 + obj_bottom = obj_z - obj_half_height + obj_top = obj_z + obj_half_height + + container_z = state.get(obj_container, "z") + container_half_height = state.get(obj_container, "height") / 2 + container_bottom = container_z - container_half_height + container_top = container_z + container_half_height + + # Check that the bottom is "above" the bottom of the container. + if obj_bottom < container_bottom - _INSIDE_Z_THRESHOLD: + return False + + # Check that the top is "below" the top of the container. + return obj_top < container_top + _INSIDE_Z_THRESHOLD def _not_inside_any_container_classifier(state: State, @@ -1462,6 +1481,13 @@ def _get_sweeping_surface_for_container(container: Object, _IsSemanticallyGreaterThan } _NONPERCEPT_PREDICATES: Set[Predicate] = set() +# NOTE: We maintain a list of predicates that we check via +# NOTE: In the future, we may include an attribute to denote whether a predicate +# is VLM perceptible or not. +# NOTE: candidates: on, inside, door opened, blocking, not blocked, ... +_VLM_EVAL_PREDICATES: { + _On, _Inside, +} ## Operators (needed in the environment for non-percept atom hack) From 3ee2ba92040385ee6847f618f4788c6fed650aef Mon Sep 17 00:00:00 2001 From: Linfeng Date: Tue, 30 Apr 2024 18:58:39 -0400 Subject: [PATCH 20/71] update State struct; adding to Spot specific subclass doesn't work, no idea why --- predicators/structs.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/predicators/structs.py b/predicators/structs.py index 4fd65ecf09..f94c543586 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -118,6 +118,19 @@ class State: # this field is provided. simulator_state: Optional[Any] = None + # DEBUG Add an additional field to store the previous atoms + prev_atoms: Optional[Sequence[GroundAtom]] = None + prev_step: Optional[Any] = None + visible_objects: Optional[Any] = None + + # DEBUG Add an additional field to store Spot images + # TODO subclass can't create new field? + # This would be directly copied from the images in raw Observation + # NOTE: This is only used when using VLM for predicate evaluation + # NOTE: Performance aspect should be considered later + camera_images: Optional[Dict[str, Any]] = None + # TODO: it's still unclear how we select and store useful images! + def __post_init__(self) -> None: # Check feature vector dimensions. for obj in self: From 1abf488b9f6486cc7529bf3387f63e2aea12764c Mon Sep 17 00:00:00 2001 From: Linfeng Date: Tue, 30 Apr 2024 20:23:14 -0400 Subject: [PATCH 21/71] add detail option --- predicators/pretrained_model_interface.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py index 3c359389bb..2e055453e5 100644 --- a/predicators/pretrained_model_interface.py +++ b/predicators/pretrained_model_interface.py @@ -258,9 +258,10 @@ def _sample_completions( class OpenAIVLM(VisionLanguageModel): """Interface for OpenAI's VLMs, including GPT-4 Turbo (and preview versions).""" - def __init__(self, model_name: str): + def __init__(self, model_name: str = "gpt-4-turbo", detail: str = "auto"): """Initialize with a specific model name.""" self.model_name = model_name + self.detail = detail self.set_openai_key() def set_openai_key(self, key: Optional[str] = None): @@ -278,6 +279,9 @@ def prepare_vision_messages( """Prepare text and image messages for the OpenAI API.""" content = [] + if detail is None or detail == "auto": + detail = self.detail + if prefix: content.append({"text": prefix, "type": "text"}) @@ -356,10 +360,18 @@ def _sample_completions( prompt = """ Describe the object relationships between the objects and containers. + You can think step by step, include predicates that are potentially true or false, and evaluate their values. You can use following predicate-style descriptions: Inside(object1, container) Blocking(object1, object2) On(object, surface) + + Example output: + Inside(hammer:object, box:container) = True + On(box:object, desk:surface) = True + On(hammer:object, desk:surface) = False + + YOUR RESPONSE: """ images = [PIL.Image.open("../test_vlm_predicate_img.jpg")] From 1c82c4440d327c686d471b845004523dbdaf045d Mon Sep 17 00:00:00 2001 From: Linfeng Date: Tue, 30 Apr 2024 20:34:25 -0400 Subject: [PATCH 22/71] working; implement On predicate with VLM classifier pipeline! add call VLM and more --- predicators/envs/spot_env.py | 139 +++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 29 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 20fb77a8a1..7b69f13fff 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -9,6 +9,7 @@ from typing import Callable, ClassVar, Collection, Dict, Iterator, List, \ Optional, Sequence, Set, Tuple, Any +import PIL.Image import matplotlib import numpy as np import pbrspot @@ -48,6 +49,7 @@ from predicators.structs import Action, EnvironmentTask, GoalDescription, \ GroundAtom, LiftedAtom, Object, Observation, Predicate, \ SpotActionExtraInfo, State, STRIPSOperator, Type, Variable +from predicators.pretrained_model_interface import OpenAIVLM ############################################################################### # Base Class # @@ -1035,6 +1037,39 @@ def _generate_goal_description(self) -> GoalDescription: """For now, we assume that there's only one goal per environment.""" +############################################################################### +# VLM Predicate Evaluation Related # +############################################################################### + +# Initialize VLM +vlm = OpenAIVLM(model_name="gpt-4-turbo", detail="auto") + +# Engineer the prompt for VLM +vlm_predicate_eval_prompt_prefix = """ +Your goal is to answer questions related to object relationships in the +given image(s). +We will use following predicate-style descriptions to ask questions: + Inside(object1, container) + Blocking(object1, object2) + On(object, surface) + +Examples: +Does this predicate hold in the following image? +Inside(apple, bowl) +Answer (in a single word): Yes/No + +Actual question: +Does this predicate hold in the following image? +{question} +Answer (in a single word): +""" + +# Provide some visual examples when needed +vlm_predicate_eval_prompt_example = "" + +# TODO: Next, try include visual hints via segmentation ("Set of Masks") + + ############################################################################### # Shared Types, Predicates, Operators # ############################################################################### @@ -1117,14 +1152,46 @@ def _on_classifier(state: State, objects: Sequence[Object]) -> bool: obj_on, obj_surface = objects currently_visible = all([o in state.visible_objects for o in objects]) - print(currently_visible, state) - + # If object not all visible and choose to use VLM, + # then use predicate values of previous time step if CFG.spot_vlm_eval_predicate and not currently_visible: # TODO: add all previous atoms to the state raise NotImplementedError + + # Call VLM to evaluate predicate value elif CFG.spot_vlm_eval_predicate and currently_visible: - # TODO call VLM to evaluate predicate value - raise NotImplementedError + predicate_str = f"On({obj_on}, {obj_surface})" + full_prompt = vlm_predicate_eval_prompt_prefix.format( + question=predicate_str + ) + + images_dict: Dict[str, RGBDImageWithContext] = state.camera_images + images = [PIL.Image.fromarray(v.rotated_rgb) for _, v in images_dict.items()] + + # Logging: prompt + logging.info(f"VLM predicate evaluation for: {predicate_str}") + logging.info(f"Prompt: {full_prompt}") + + vlm_responses = vlm.sample_completions( + prompt=full_prompt, + imgs=images, + temperature=0.2, + seed=int(time.time()), + num_completions=1, + ) + + # Logging + logging.info(f"VLM response 0: {vlm_responses[0]}") + + vlm_response = vlm_responses[0].strip().lower() + if vlm_response == "yes": + return True + elif vlm_response == "no": + return False + else: + logging.error(f"VLM response not understood: {vlm_response}. Treat as False.") + return False + else: # Check that the bottom of the object is close to the top of the surface. expect = state.get(obj_surface, "z") + state.get(obj_surface, "height") / 2 @@ -1154,35 +1221,49 @@ def _inside_classifier(state: State, objects: Sequence[Object]) -> bool: print(currently_visible, state) - if CFG.spot_vlm_eval_predicate and not currently_visible: - # TODO: add all previous atoms to the state - raise NotImplementedError - elif CFG.spot_vlm_eval_predicate and currently_visible: - # TODO call VLM to evaluate predicate value - raise NotImplementedError - - else: - - if not _object_in_xy_classifier( - state, obj_in, obj_container, buffer=_INSIDE_SURFACE_BUFFER): - return False + # if CFG.spot_vlm_eval_predicate and not currently_visible: + # # TODO: add all previous atoms to the state + # # TODO: then we just use the atom value from the last state + # raise NotImplementedError + # elif CFG.spot_vlm_eval_predicate and currently_visible: + # # TODO call VLM to evaluate predicate value + # full_prompt = vlm_predicate_eval_prompt_prefix.format( + # question=f"Inside({obj_in}, {obj_container})" + # ) + # images = state.camera_images + # + # vlm_responses = vlm.sample_completions( + # prompt=full_prompt, + # imgs=images, + # temperature=0.2, + # seed=int(time.time()), + # num_completions=1, + # ) + # vlm_response = vlm_responses[0].strip().lower() + # raise NotImplementedError + # + # else: + + if not _object_in_xy_classifier( + state, obj_in, obj_container, buffer=_INSIDE_SURFACE_BUFFER): + return False - obj_z = state.get(obj_in, "z") - obj_half_height = state.get(obj_in, "height") / 2 - obj_bottom = obj_z - obj_half_height - obj_top = obj_z + obj_half_height + obj_z = state.get(obj_in, "z") + obj_half_height = state.get(obj_in, "height") / 2 + obj_bottom = obj_z - obj_half_height + obj_top = obj_z + obj_half_height - container_z = state.get(obj_container, "z") - container_half_height = state.get(obj_container, "height") / 2 - container_bottom = container_z - container_half_height - container_top = container_z + container_half_height + container_z = state.get(obj_container, "z") + container_half_height = state.get(obj_container, "height") / 2 + container_bottom = container_z - container_half_height + container_top = container_z + container_half_height - # Check that the bottom is "above" the bottom of the container. - if obj_bottom < container_bottom - _INSIDE_Z_THRESHOLD: - return False + # Check that the bottom is "above" the bottom of the container. + if obj_bottom < container_bottom - _INSIDE_Z_THRESHOLD: + return False - # Check that the top is "below" the top of the container. - return obj_top < container_top + _INSIDE_Z_THRESHOLD + # Check that the top is "below" the top of the container. + return obj_top < container_top + _INSIDE_Z_THRESHOLD def _not_inside_any_container_classifier(state: State, From eeb1583ac9fb9e2f9334f9b3a3ba9eebf279de8a Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 1 May 2024 15:44:58 -0400 Subject: [PATCH 23/71] make a separate function for vlm predicate classifier evaluation --- predicators/envs/spot_env.py | 180 ++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 86 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 7b69f13fff..98c431bb22 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -308,7 +308,7 @@ def percept_predicates(self) -> Set[Predicate]: def action_space(self) -> Box: # The action space is effectively empty because only the extra info # part of actions are used. - return Box(0, 1, (0, )) + return Box(0, 1, (0,)) @abc.abstractmethod def _get_dry_task(self, train_or_test: str, @@ -346,7 +346,7 @@ def _get_next_dry_observation( nonpercept_atoms) if action_name in [ - "MoveToReachObject", "MoveToReadySweep", "MoveToBodyViewObject" + "MoveToReachObject", "MoveToReadySweep", "MoveToBodyViewObject" ]: robot_rel_se2_pose = action_args[1] return _dry_simulate_move_to_reach_obj(obs, robot_rel_se2_pose, @@ -713,7 +713,7 @@ def _build_realworld_observation( for swept_object in swept_objects: if swept_object not in all_objects_in_view: if container is not None and container in \ - all_objects_in_view: + all_objects_in_view: while True: msg = ( f"\nATTENTION! The {swept_object.name} was not " @@ -998,7 +998,7 @@ def _actively_construct_initial_object_views( return obj_to_se3_pose def _run_init_search_for_objects( - self, detection_ids: Set[ObjectDetectionID] + self, detection_ids: Set[ObjectDetectionID] ) -> Dict[ObjectDetectionID, math_helpers.SE3Pose]: """Have the hand look down from high up at first.""" assert self._robot is not None @@ -1066,10 +1066,39 @@ def _generate_goal_description(self) -> GoalDescription: # Provide some visual examples when needed vlm_predicate_eval_prompt_example = "" - # TODO: Next, try include visual hints via segmentation ("Set of Masks") +def vlm_predicate_classify(question: str, state: State) -> bool: + """Use VLM to evaluate (classify) a predicate in a given state.""" + full_prompt = vlm_predicate_eval_prompt_prefix.format( + question=question + ) + images_dict: Dict[str, RGBDImageWithContext] = state.camera_images + images = [PIL.Image.fromarray(v.rotated_rgb) for _, v in images_dict.items()] + + logging.info(f"VLM predicate evaluation for: {question}") + logging.info(f"Prompt: {full_prompt}") + + vlm_responses = vlm.sample_completions( + prompt=full_prompt, + imgs=images, + temperature=0.2, + seed=int(time.time()), + num_completions=1, + ) + logging.info(f"VLM response 0: {vlm_responses[0]}") + + vlm_response = vlm_responses[0].strip().lower() + if vlm_response == "yes": + return True + elif vlm_response == "no": + return False + else: + logging.error(f"VLM response not understood: {vlm_response}. Treat as False.") + return False + + ############################################################################### # Shared Types, Predicates, Operators # ############################################################################### @@ -1133,8 +1162,8 @@ def _object_in_xy_classifier(state: State, spot, = state.get_objects(_robot_type) if obj1.is_instance(_movable_object_type) and \ - _is_placeable_classifier(state, [obj1]) and \ - _holding_classifier(state, [spot, obj1]): + _is_placeable_classifier(state, [obj1]) and \ + _holding_classifier(state, [spot, obj1]): return False # Check that the center of the object is contained within the surface in @@ -1150,8 +1179,8 @@ def _object_in_xy_classifier(state: State, def _on_classifier(state: State, objects: Sequence[Object]) -> bool: obj_on, obj_surface = objects - currently_visible = all([o in state.visible_objects for o in objects]) + currently_visible = all([o in state.visible_objects for o in objects]) # If object not all visible and choose to use VLM, # then use predicate values of previous time step if CFG.spot_vlm_eval_predicate and not currently_visible: @@ -1160,37 +1189,11 @@ def _on_classifier(state: State, objects: Sequence[Object]) -> bool: # Call VLM to evaluate predicate value elif CFG.spot_vlm_eval_predicate and currently_visible: - predicate_str = f"On({obj_on}, {obj_surface})" - full_prompt = vlm_predicate_eval_prompt_prefix.format( - question=predicate_str - ) - - images_dict: Dict[str, RGBDImageWithContext] = state.camera_images - images = [PIL.Image.fromarray(v.rotated_rgb) for _, v in images_dict.items()] - - # Logging: prompt - logging.info(f"VLM predicate evaluation for: {predicate_str}") - logging.info(f"Prompt: {full_prompt}") - - vlm_responses = vlm.sample_completions( - prompt=full_prompt, - imgs=images, - temperature=0.2, - seed=int(time.time()), - num_completions=1, - ) - - # Logging - logging.info(f"VLM response 0: {vlm_responses[0]}") - - vlm_response = vlm_responses[0].strip().lower() - if vlm_response == "yes": - return True - elif vlm_response == "no": - return False - else: - logging.error(f"VLM response not understood: {vlm_response}. Treat as False.") - return False + predicate_str = f""" + On({obj_on}, {obj_surface}) + (Whether {obj_on} is on {obj_surface} in the image?) + """ + return vlm_predicate_classify(predicate_str, state) else: # Check that the bottom of the object is close to the top of the surface. @@ -1217,53 +1220,43 @@ def _top_above_classifier(state: State, objects: Sequence[Object]) -> bool: def _inside_classifier(state: State, objects: Sequence[Object]) -> bool: obj_in, obj_container = objects + currently_visible = all([o in state.visible_objects for o in objects]) + # If object not all visible and choose to use VLM, + # then use predicate values of previous time step + if CFG.spot_vlm_eval_predicate and not currently_visible: + # TODO: add all previous atoms to the state + raise NotImplementedError - print(currently_visible, state) - - # if CFG.spot_vlm_eval_predicate and not currently_visible: - # # TODO: add all previous atoms to the state - # # TODO: then we just use the atom value from the last state - # raise NotImplementedError - # elif CFG.spot_vlm_eval_predicate and currently_visible: - # # TODO call VLM to evaluate predicate value - # full_prompt = vlm_predicate_eval_prompt_prefix.format( - # question=f"Inside({obj_in}, {obj_container})" - # ) - # images = state.camera_images - # - # vlm_responses = vlm.sample_completions( - # prompt=full_prompt, - # imgs=images, - # temperature=0.2, - # seed=int(time.time()), - # num_completions=1, - # ) - # vlm_response = vlm_responses[0].strip().lower() - # raise NotImplementedError - # - # else: - - if not _object_in_xy_classifier( - state, obj_in, obj_container, buffer=_INSIDE_SURFACE_BUFFER): - return False + # Call VLM to evaluate predicate value + elif CFG.spot_vlm_eval_predicate and currently_visible: + predicate_str = f""" + Inside({obj_in}, {obj_container}) + (Whether {obj_in} is inside {obj_container} in the image?) + """ + return vlm_predicate_classify(predicate_str, state) - obj_z = state.get(obj_in, "z") - obj_half_height = state.get(obj_in, "height") / 2 - obj_bottom = obj_z - obj_half_height - obj_top = obj_z + obj_half_height + else: + if not _object_in_xy_classifier( + state, obj_in, obj_container, buffer=_INSIDE_SURFACE_BUFFER): + return False - container_z = state.get(obj_container, "z") - container_half_height = state.get(obj_container, "height") / 2 - container_bottom = container_z - container_half_height - container_top = container_z + container_half_height + obj_z = state.get(obj_in, "z") + obj_half_height = state.get(obj_in, "height") / 2 + obj_bottom = obj_z - obj_half_height + obj_top = obj_z + obj_half_height - # Check that the bottom is "above" the bottom of the container. - if obj_bottom < container_bottom - _INSIDE_Z_THRESHOLD: - return False + container_z = state.get(obj_container, "z") + container_half_height = state.get(obj_container, "height") / 2 + container_bottom = container_z - container_half_height + container_top = container_z + container_half_height - # Check that the top is "below" the top of the container. - return obj_top < container_top + _INSIDE_Z_THRESHOLD + # Check that the bottom is "above" the bottom of the container. + if obj_bottom < container_bottom - _INSIDE_Z_THRESHOLD: + return False + + # Check that the top is "below" the top of the container. + return obj_top < container_top + _INSIDE_Z_THRESHOLD def _not_inside_any_container_classifier(state: State, @@ -1312,8 +1305,8 @@ def in_general_view_classifier(state: State, def _obj_reachable_from_spot_pose(spot_pose: math_helpers.SE3Pose, obj_position: math_helpers.Vec3) -> bool: is_xy_near = np.sqrt( - (spot_pose.x - obj_position.x)**2 + - (spot_pose.y - obj_position.y)**2) <= _REACHABLE_THRESHOLD + (spot_pose.x - obj_position.x) ** 2 + + (spot_pose.y - obj_position.y) ** 2) <= _REACHABLE_THRESHOLD # Compute angle between spot's forward direction and the line from # spot to the object. @@ -1355,6 +1348,21 @@ def _blocking_classifier(state: State, objects: Sequence[Object]) -> bool: if blocker_obj == blocked_obj: return False + currently_visible = all([o in state.visible_objects for o in objects]) + # If object not all visible and choose to use VLM, + # then use predicate values of previous time step + if CFG.spot_vlm_eval_predicate and not currently_visible: + # TODO: add all previous atoms to the state + raise NotImplementedError + + # Call VLM to evaluate predicate value + elif CFG.spot_vlm_eval_predicate and currently_visible: + predicate_str = f""" + (Whether {blocker_obj} is blocking {blocked_obj} for further manipulation in the image?) + Blocking({blocker_obj}, {blocked_obj}) + """ + return vlm_predicate_classify(predicate_str, state) + # Only consider draggable (non-placeable, movable) objects to be blockers. if not blocker_obj.is_instance(_movable_object_type): return False @@ -1369,7 +1377,7 @@ def _blocking_classifier(state: State, objects: Sequence[Object]) -> bool: spot, = state.get_objects(_robot_type) if blocked_obj.is_instance(_movable_object_type) and \ - _holding_classifier(state, [spot, blocked_obj]): + _holding_classifier(state, [spot, blocked_obj]): return False # Draw a line between blocked and the robot’s current pose. @@ -1439,8 +1447,8 @@ def _container_adjacent_to_surface_for_sweeping(container: Object, container_x = state.get(container, "x") container_y = state.get(container, "y") - dist = np.sqrt((expected_x - container_x)**2 + - (expected_y - container_y)**2) + dist = np.sqrt((expected_x - container_x) ** 2 + + (expected_y - container_y) ** 2) return dist <= _CONTAINER_SWEEP_READY_BUFFER @@ -2389,7 +2397,7 @@ def _dry_simulate_sweep_into_container( x = container_pose.x + dx y = container_pose.y + dy z = container_pose.z - dist_to_container = (dx**2 + dy**2)**0.5 + dist_to_container = (dx ** 2 + dy ** 2) ** 0.5 assert dist_to_container > (container_radius + _INSIDE_SURFACE_BUFFER) From acbdb0ab26aba1599e3fbfbb4abd075085ae333a Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 3 May 2024 15:33:55 -0400 Subject: [PATCH 24/71] add test --- tests/test_pretrained_model_interface.py | 38 +++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/test_pretrained_model_interface.py b/tests/test_pretrained_model_interface.py index 13056ddacd..1c20b4ae25 100644 --- a/tests/test_pretrained_model_interface.py +++ b/tests/test_pretrained_model_interface.py @@ -8,7 +8,7 @@ from predicators import utils from predicators.pretrained_model_interface import GoogleGeminiVLM, \ - LargeLanguageModel, OpenAILLM, VisionLanguageModel + LargeLanguageModel, OpenAILLM, OpenAIVLM, VisionLanguageModel class _DummyLLM(LargeLanguageModel): @@ -147,3 +147,39 @@ def test_gemini_vlm(): # Create an OpenAILLM with the curie model. vlm = GoogleGeminiVLM("gemini-pro-vision") assert vlm.get_id() == "Google-gemini-pro-vision" + + +def test_openai_vlm(): + """Tests for GoogleGeminiVLM().""" + cache_dir = "_fake_llm_cache_dir" + utils.reset_config({"pretrained_model_prompt_cache_dir": cache_dir}) + if "OPENAI_API_KEY" not in os.environ: # pragma: no cover + os.environ["OPENAI_API_KEY"] = "dummy API key" + # Create an OpenAILLM with the curie model. + vlm = OpenAIVLM("gpt-4-turbo") + assert vlm.get_id() == "OpenAI-gpt-4-turbo" + + +def test_openai_vlm_image_example(): + # Make sure the OPENAI_API_KEY is set + model_name = "gpt-4-turbo" + vlm = OpenAIVLM(model_name) + + prompt = """ + Describe the object relationships between the objects and containers. + You can use following predicate-style descriptions: + Inside(object1, container) + Blocking(object1, object2) + On(object, surface) + """ + images = [Image.open("../tests/datasets/test_vlm_predicate_img.jpg")] + + # NOTE: Uncomment for actual test + # print("Start requesting...") + # completions = vlm.sample_completions(prompt=prompt, + # imgs=images, + # temperature=0.5, + # num_completions=3, + # seed=0) + # for i, completion in enumerate(completions): + # print(f"Completion {i + 1}: \n{completion}\n") \ No newline at end of file From 01498f60172abd5278ee3d8caa1a48b0f222258d Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 3 May 2024 16:30:51 -0400 Subject: [PATCH 25/71] update example, move to test, move img --- .../datasets/test_vlm_predicate_img.jpg | Bin tests/test_pretrained_model_interface.py | 24 ++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) rename test_vlm_predicate_img.jpg => tests/datasets/test_vlm_predicate_img.jpg (100%) diff --git a/test_vlm_predicate_img.jpg b/tests/datasets/test_vlm_predicate_img.jpg similarity index 100% rename from test_vlm_predicate_img.jpg rename to tests/datasets/test_vlm_predicate_img.jpg diff --git a/tests/test_pretrained_model_interface.py b/tests/test_pretrained_model_interface.py index 1c20b4ae25..98ecc307ef 100644 --- a/tests/test_pretrained_model_interface.py +++ b/tests/test_pretrained_model_interface.py @@ -167,19 +167,27 @@ def test_openai_vlm_image_example(): prompt = """ Describe the object relationships between the objects and containers. + You can think step by step, include predicates that are potentially true or false, and evaluate their values. You can use following predicate-style descriptions: Inside(object1, container) Blocking(object1, object2) On(object, surface) + + Example output: + Inside(hammer:object, box:container) = True + On(box:object, desk:surface) = True + On(hammer:object, desk:surface) = False + + YOUR RESPONSE: """ images = [Image.open("../tests/datasets/test_vlm_predicate_img.jpg")] # NOTE: Uncomment for actual test - # print("Start requesting...") - # completions = vlm.sample_completions(prompt=prompt, - # imgs=images, - # temperature=0.5, - # num_completions=3, - # seed=0) - # for i, completion in enumerate(completions): - # print(f"Completion {i + 1}: \n{completion}\n") \ No newline at end of file + print("Start requesting...") + completions = vlm.sample_completions(prompt=prompt, + imgs=images, + temperature=0.5, + num_completions=3, + seed=0) + for i, completion in enumerate(completions): + print(f"Completion {i + 1}: \n{completion}\n") \ No newline at end of file From 0774354eb0f90318bde01fdb205011cab9ecaa41 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 3 May 2024 16:30:58 -0400 Subject: [PATCH 26/71] remove --- predicators/pretrained_model_interface.py | 33 ----------------------- 1 file changed, 33 deletions(-) diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py index 2e055453e5..4ca788db6d 100644 --- a/predicators/pretrained_model_interface.py +++ b/predicators/pretrained_model_interface.py @@ -350,36 +350,3 @@ def _sample_completions( for _ in range(num_completions) ] return responses - - -# Example usage: -if __name__ == "__main__": - # Make sure the OPENAI_API_KEY is set - model_name = "gpt-4-turbo" - vlm = OpenAIVLM(model_name) - - prompt = """ - Describe the object relationships between the objects and containers. - You can think step by step, include predicates that are potentially true or false, and evaluate their values. - You can use following predicate-style descriptions: - Inside(object1, container) - Blocking(object1, object2) - On(object, surface) - - Example output: - Inside(hammer:object, box:container) = True - On(box:object, desk:surface) = True - On(hammer:object, desk:surface) = False - - YOUR RESPONSE: - """ - images = [PIL.Image.open("../test_vlm_predicate_img.jpg")] - - print("Start requesting...") - completions = vlm.sample_completions( - prompt=prompt, - imgs=images, - temperature=0.5, num_completions=3, seed=0 - ) - for i, completion in enumerate(completions): - print(f"Completion {i + 1}: \n{completion}\n") From 68ef57df97e1e52c33905e79ab5338ac263f1338 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 3 May 2024 21:01:51 -0400 Subject: [PATCH 27/71] format --- predicators/pretrained_model_interface.py | 54 ++++++++++++++--------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py index 4ca788db6d..08dcdc6e71 100644 --- a/predicators/pretrained_model_interface.py +++ b/predicators/pretrained_model_interface.py @@ -256,7 +256,8 @@ def _sample_completions( class OpenAIVLM(VisionLanguageModel): - """Interface for OpenAI's VLMs, including GPT-4 Turbo (and preview versions).""" + """Interface for OpenAI's VLMs, including GPT-4 Turbo (and preview + versions).""" def __init__(self, model_name: str = "gpt-4-turbo", detail: str = "auto"): """Initialize with a specific model name.""" @@ -271,11 +272,12 @@ def set_openai_key(self, key: Optional[str] = None): key = os.environ["OPENAI_API_KEY"] openai.api_key = key - def prepare_vision_messages( - self, images: List[PIL.Image.Image], - prefix: Optional[str] = None, suffix: Optional[str] = None, image_size: Optional[int] = 512, - detail: str = "auto" - ): + def prepare_vision_messages(self, + images: List[PIL.Image.Image], + prefix: Optional[str] = None, + suffix: Optional[str] = None, + image_size: Optional[int] = 512, + detail: str = "auto"): """Prepare text and image messages for the OpenAI API.""" content = [] @@ -291,7 +293,8 @@ def prepare_vision_messages( img_resized = img if image_size: factor = image_size / max(img.size) - img_resized = img.resize((int(img.size[0] * factor), int(img.size[1] * factor))) + img_resized = img.resize( + (int(img.size[0] * factor), int(img.size[1] * factor))) # Convert the image to PNG format and encode it in base64 buffer = BytesIO() @@ -312,9 +315,15 @@ def prepare_vision_messages( return [{"role": "user", "content": content}] - @retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6)) - def call_openai_api(self, messages: list, model: str = "gpt-4", seed: Optional[int] = None, max_tokens: int = 32, - temperature: float = 0.2, verbose: bool = False): + @retry(wait=wait_random_exponential(min=1, max=60), + stop=stop_after_attempt(6)) + def call_openai_api(self, + messages: list, + model: str = "gpt-4", + seed: Optional[int] = None, + max_tokens: int = 32, + temperature: float = 0.2, + verbose: bool = False): """Make an API call to OpenAI.""" client = openai.OpenAI() completion = client.chat.completions.create( @@ -334,19 +343,24 @@ def get_id(self) -> str: return f"OpenAI-{self.model_name}" def _sample_completions( - self, - prompt: str, - imgs: Optional[List[PIL.Image.Image]], - temperature: float, - seed: int, - stop_token: Optional[str] = None, - num_completions: int = 1, - max_tokens=512, + self, + prompt: str, + imgs: Optional[List[PIL.Image.Image]], + temperature: float, + seed: int, + stop_token: Optional[str] = None, + num_completions: int = 1, + max_tokens=512, ) -> List[str]: """Query the model and get responses.""" - messages = self.prepare_vision_messages(prefix=prompt, images=imgs, detail="auto") + messages = self.prepare_vision_messages(prefix=prompt, + images=imgs, + detail="auto") responses = [ - self.call_openai_api(messages, model=self.model_name, max_tokens=max_tokens, temperature=temperature) + self.call_openai_api(messages, + model=self.model_name, + max_tokens=max_tokens, + temperature=temperature) for _ in range(num_completions) ] return responses From 6560a9435dc3c0716f960cb8f27dc02c205f9565 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 3 May 2024 21:11:29 -0400 Subject: [PATCH 28/71] update --- predicators/envs/spot_env.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 98c431bb22..19c9b7fa50 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Callable, ClassVar, Collection, Dict, Iterator, List, \ - Optional, Sequence, Set, Tuple, Any + Optional, Sequence, Set, Tuple import PIL.Image import matplotlib @@ -22,6 +22,7 @@ from predicators import utils from predicators.envs import BaseEnv +from predicators.pretrained_model_interface import OpenAIVLM from predicators.settings import CFG from predicators.spot_utils.perception.object_detection import \ AprilTagObjectDetectionID, KnownStaticObjectDetectionID, \ @@ -49,7 +50,6 @@ from predicators.structs import Action, EnvironmentTask, GoalDescription, \ GroundAtom, LiftedAtom, Object, Observation, Predicate, \ SpotActionExtraInfo, State, STRIPSOperator, Type, Variable -from predicators.pretrained_model_interface import OpenAIVLM ############################################################################### # Base Class # @@ -96,12 +96,7 @@ class _PartialPerceptionState(State): in the classifier definitions for the dummy predicates """ - # # DEBUG Add an additional field to store Spot images - # # This would be directly copied from the images in raw Observation - # # NOTE: This is only used when using VLM for predicate evaluation - # # NOTE: Performance aspect should be considered later - # cam_images: Optional[Dict[str, RGBDImageWithContext]] = None - # # TODO: it's still unclear how we select and store useful images! + # obs_images: Optional[Dict[str, RGBDImageWithContext]] = None @property def _simulator_state_predicates(self) -> Set[Predicate]: @@ -1071,11 +1066,11 @@ def _generate_goal_description(self) -> GoalDescription: def vlm_predicate_classify(question: str, state: State) -> bool: """Use VLM to evaluate (classify) a predicate in a given state.""" - full_prompt = vlm_predicate_eval_prompt_prefix.format( - question=question - ) + full_prompt = vlm_predicate_eval_prompt_prefix.format(question=question) images_dict: Dict[str, RGBDImageWithContext] = state.camera_images - images = [PIL.Image.fromarray(v.rotated_rgb) for _, v in images_dict.items()] + images = [ + PIL.Image.fromarray(v.rotated_rgb) for _, v in images_dict.items() + ] logging.info(f"VLM predicate evaluation for: {question}") logging.info(f"Prompt: {full_prompt}") @@ -1095,7 +1090,8 @@ def vlm_predicate_classify(question: str, state: State) -> bool: elif vlm_response == "no": return False else: - logging.error(f"VLM response not understood: {vlm_response}. Treat as False.") + logging.error( + f"VLM response not understood: {vlm_response}. Treat as False.") return False @@ -1197,7 +1193,8 @@ def _on_classifier(state: State, objects: Sequence[Object]) -> bool: else: # Check that the bottom of the object is close to the top of the surface. - expect = state.get(obj_surface, "z") + state.get(obj_surface, "height") / 2 + expect = state.get(obj_surface, + "z") + state.get(obj_surface, "height") / 2 actual = state.get(obj_on, "z") - state.get(obj_on, "height") / 2 classification_val = abs(actual - expect) < _ONTOP_Z_THRESHOLD @@ -1575,7 +1572,8 @@ def _get_sweeping_surface_for_container(container: Object, # is VLM perceptible or not. # NOTE: candidates: on, inside, door opened, blocking, not blocked, ... _VLM_EVAL_PREDICATES: { - _On, _Inside, + _On, + _Inside, } From 1596f68a3a84a7be0c6f4da166174c7fa0d2f63e Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 16:40:49 -0400 Subject: [PATCH 29/71] batch VLM classifier working on Spot!! add field to State, add VLMPredicate and VLMGroundAtom --- predicators/structs.py | 86 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/predicators/structs.py b/predicators/structs.py index f94c543586..f2a01dcef4 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -118,18 +118,14 @@ class State: # this field is provided. simulator_state: Optional[Any] = None - # DEBUG Add an additional field to store the previous atoms - prev_atoms: Optional[Sequence[GroundAtom]] = None - prev_step: Optional[Any] = None + # Store additional fields for VLM predicate classifiers + # NOTE: adding in Spot ev subclass doesn't work; may need fix + # prev_atoms: Optional[Dict[str, bool]] = None + vlm_atom_dict: Optional[Dict[VLMGroundAtom, bool or None]] = None + vlm_predicates: Optional[Collection[Predicate]] = None visible_objects: Optional[Any] = None - - # DEBUG Add an additional field to store Spot images - # TODO subclass can't create new field? - # This would be directly copied from the images in raw Observation - # NOTE: This is only used when using VLM for predicate evaluation - # NOTE: Performance aspect should be considered later + # This is directly copied from the images in raw Observation camera_images: Optional[Dict[str, Any]] = None - # TODO: it's still unclear how we select and store useful images! def __post_init__(self) -> None: # Check feature vector dimensions. @@ -320,6 +316,40 @@ def _negated_classifier(self, state: State, return not self._classifier(state, objects) +class VLMPredicate(Predicate): + """Struct defining a predicate (a lifted classifier over states) that uses + a VLM for evaluation. + + It overrides the `holds` method, which only return the stored predicate + value in the State. Instead, it supports a query method that generates VLM + query, where all VLM predicates will be evaluated at once. + """ + + def get_query(self, objects: Sequence[Object]) -> str: + """Get a query string for this predicate. + + Instead of directly evaluating the predicate, we will use the VLM to + evaluate all VLM predicate classifiers in a batched manner. + """ + self.pddl_str() + + def holds(self, state: State, objects: Sequence[Object]) -> bool: + """Public method for getting predicate value. + + Performs type checking first. Directly use value + """ + assert len(objects) == self.arity + for obj, pred_type in zip(objects, self.types): + assert isinstance(obj, Object) + assert obj.is_instance(pred_type) + + # TODO get predicate values from State + # TODO store a dict, from str to bool; but it should be str of GroundAtom or Predicate? + # return state.prev_atoms[str(self)] + # return self._classifier(state, objects) + return state.vlm_atom_dict[VLMGroundAtom(self, objects)] + + @dataclass(frozen=True, repr=False, eq=False) class _Atom: """Struct defining an atom (a predicate applied to either variables or @@ -427,6 +457,42 @@ def holds(self, state: State) -> bool: return self.predicate.holds(state, self.objects) +@dataclass(frozen=True, repr=False, eq=False) +class VLMGroundAtom(GroundAtom): + """Struct defining a ground atom (a predicate applied to objects) that uses + a VLM for evaluation. + + It overrides the `holds` method, which only return the stored predicate + value in the State. Instead, it supports a query method that generates VLM + query, where all VLM predicates will be evaluated at once. + """ + + # NOTE: This subclasses GroundAtom to support VLM predicates and classifiers + predicate: VLMPredicate + + def get_query_str(self, without_type: bool = False) -> str: + """Get a query string for this ground atom. + + Instead of directly evaluating the ground atom, we will use the VLM to + evaluate all VLM predicate classifiers in a batched manner. + """ + if without_type: + string = self.predicate.name + "(" + ", ".join(o.name for o in self.objects) + ")" + else: + string = str(self) + return string + + def holds(self, state: State) -> bool: + """Public method for getting predicate value. + + Retrieve GroundAtom value from State directly. + """ + assert isinstance(self.predicate, VLMPredicate) + # TODO get predicate values from State + return state.vlm_atom_dict[self] + # return self.predicate.holds(state, self.objects) + + @dataclass(frozen=True, eq=False) class Task: """Struct defining a task, which is an initial state and goal.""" From 98ae2a9660bb069f0918e9ee376c76a0800a7962 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 16:41:50 -0400 Subject: [PATCH 30/71] batch VLM classifier eval: add vlm predicates fields to observation --- predicators/perception/spot_perceiver.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/predicators/perception/spot_perceiver.py b/predicators/perception/spot_perceiver.py index 814ab04193..40682e571d 100644 --- a/predicators/perception/spot_perceiver.py +++ b/predicators/perception/spot_perceiver.py @@ -205,6 +205,8 @@ def _update_state_from_observation(self, observation: Observation) -> None: if CFG.spot_vlm_eval_predicate: # Add current Spot images to the state if needed self._camera_images = observation.images + self._vlm_atom_dict = observation.vlm_atom_dict + self._vlm_predicates = observation.vlm_predicates def _create_state(self) -> State: if self._waiting_for_observation: @@ -294,7 +296,10 @@ def _create_state(self) -> State: state = _PartialPerceptionState(percept_state.data, simulator_state=simulator_state, camera_images=camera_images, - visible_objects=self._objects_in_view) + visible_objects=self._objects_in_view, + vlm_atom_dict=self._vlm_atom_dict, + vlm_predicates=self._vlm_predicates, + ) # DEBUG - look into dataclass field init - why warning return state From 2683a6bdcdb495d8e883f5662ebb278789ea0a43 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 16:42:48 -0400 Subject: [PATCH 31/71] batch VLM classifier eval: function on batch query and parse --- .../perception/object_perception.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 predicators/spot_utils/perception/object_perception.py diff --git a/predicators/spot_utils/perception/object_perception.py b/predicators/spot_utils/perception/object_perception.py new file mode 100644 index 0000000000..bcd3cf3d1f --- /dev/null +++ b/predicators/spot_utils/perception/object_perception.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import logging +import time +from typing import Dict, List, Set, Sequence + +import PIL.Image + +from predicators.pretrained_model_interface import OpenAIVLM +from predicators.spot_utils.perception.perception_structs import RGBDImageWithContext +from predicators.structs import State, VLMGroundAtom, Object, VLMPredicate +from predicators.utils import get_object_combinations + +############################################################################### +# VLM Predicate Evaluation Related # +############################################################################### + +# Initialize VLM +vlm = OpenAIVLM(model_name="gpt-4-turbo", detail="auto") + +# Engineer the prompt for VLM +vlm_predicate_eval_prompt = """ +Your goal is to answer questions related to object relationships in the +given image(s) from the cameras of a Spot robot. +We will use following predicate-style descriptions to ask questions: + Inside(object1, container) + Blocking(object1, object2) + On(object, surface) + +Examples: +Does this predicate hold in the following image? +Inside(apple, bowl) +Answer (in a single word): Yes/No + +Actual question: +Does this predicate hold in the following image? +{question} +Answer (in a single word): +""" + +vlm_predicate_batch_eval_prompt = """ +Your goal is to answer questions related to object relationships in the +given image(s) from the cameras of a Spot robot. Each question is independent +while all questions rely on the same set of Spot images at a certain moment. +We will use following predicate-style descriptions to ask questions: + Inside(object1, container) + Blocking(object1, object2) + On(object, surface) + +Examples (separated by line or newline character): +Do these predicates hold in the following images? +Inside(apple:object, bowl:container) +On(apple:object, table:surface) +Blocking(apple:object, orange:object) +Blocking(apple:object, apple:object) +On(apple:object, apple:object) + +Answer (in a single word Yes/No/Unknown for each question, unknown if can't tell from given images): +Yes +No +Unknown +No +No + +Actual questions (separated by line or newline character): +Do these predicates hold in the following images? +{question} + +Answer (in a single word Yes/No for each question): +""" + +# Provide some visual examples when needed +vlm_predicate_eval_prompt_example = "" + + +def vlm_predicate_classify(question: str, state: State) -> bool | None: + """Use VLM to evaluate (classify) a predicate in a given state. + + TODO: Next, try include visual hints via segmentation ("Set of Masks") + """ + full_prompt = vlm_predicate_eval_prompt.format(question=question) + images_dict: Dict[str, RGBDImageWithContext] = state.camera_images + images = [ + PIL.Image.fromarray(v.rotated_rgb) for _, v in images_dict.items() + ] + + logging.info(f"VLM predicate evaluation for: {question}") + logging.info(f"Prompt: {full_prompt}") + + vlm_responses = vlm.sample_completions( + prompt=full_prompt, + imgs=images, + temperature=0.2, + seed=int(time.time()), + num_completions=1, + ) + logging.info(f"VLM response 0: {vlm_responses[0]}") + + vlm_response = vlm_responses[0].strip().lower() + if vlm_response == "yes": + return True + elif vlm_response == "no": + return False + elif vlm_response == "unknown": + return None + else: + logging.error( + f"VLM response not understood: {vlm_response}. Treat as None.") + return None + + +def vlm_predicate_batch_query(queries: List[str], images: Dict[str, RGBDImageWithContext]) -> List[bool]: + """Use queries generated from VLM predicates to evaluate them via VLM in batch. + + The VLM takes a list of queries and images in current observation to evaluate them. + """ + + # Assemble the full prompt + question = '\n'.join(queries) + full_prompt = vlm_predicate_batch_eval_prompt.format(question=question) + + image_list = [ + PIL.Image.fromarray(v.rotated_rgb) for _, v in images.items() + ] + + logging.info(f"VLM predicate evaluation for: {question}") + logging.info(f"Prompt: {full_prompt}") + + vlm_responses = vlm.sample_completions( + prompt=full_prompt, + imgs=image_list, + temperature=0.2, + seed=int(time.time()), + num_completions=1, + ) + logging.info(f"VLM response 0: {vlm_responses[0]}") + + # Parse the responses + responses = vlm_responses[0].strip().lower().split('\n') + results = [] + for i, r in enumerate(responses): + assert r in ['yes', 'no', 'unknown'], f"Invalid response in line {i}: {r}" + if r == 'yes': + results.append(True) + elif r == 'no': + results.append(False) + else: + results.append(None) + assert len(results) == len(queries), "Number of responses should match queries." + + return results + + +def vlm_predicate_batch_classify(atoms: Set[VLMGroundAtom], images: Dict[str, RGBDImageWithContext], + get_dict: bool = True) -> Dict[VLMGroundAtom, bool] | Set[VLMGroundAtom]: + """Use VLM to evaluate a set of atoms in a given state.""" + # Get the queries for the atoms + queries = [atom.get_query_str() for atom in atoms] + + if len(queries) == 0: + return {} + + logging.info(f"VLM predicate evaluation queries: {queries}") + + # Call VLM to evaluate the queries + results = vlm_predicate_batch_query(queries, images) + + # Update the atoms with the results + if get_dict: + return {atom: result for atom, result in zip(atoms, results)} + else: + hold_atoms = set() + for atom, result in zip(atoms, results): + if result: + hold_atoms.add(atom) + return hold_atoms + + +def get_vlm_atom_combinations(objects: Sequence[Object], preds: Set[VLMPredicate]) -> Set[VLMGroundAtom]: + atoms = set() + for pred in preds: + for choice in get_object_combinations(objects, pred.types): + atoms.add(VLMGroundAtom(pred, choice)) + return atoms From b0b75685ea9409b26cacadf5f37dba797a426380 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 16:46:28 -0400 Subject: [PATCH 32/71] batch VLM classifier eval: provide VLM predicates to object finding, evaluate all VLM predicates on all objects to build init obs for env reset --- .../spot_utils/skills/spot_find_objects.py | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index ce8db3826d..7df9a69ad5 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -1,16 +1,19 @@ """Interface for finding objects by moving around and running detection.""" - +import logging import time -from typing import Any, Collection, Dict, List, Optional, Sequence, Tuple +from collections import defaultdict +from typing import Any, Collection, Dict, List, Optional, Sequence, Tuple, Set, Callable import numpy as np from bosdyn.client import math_helpers from bosdyn.client.lease import LeaseClient +from bosdyn.client.math_helpers import SE3Pose from bosdyn.client.sdk import Robot from scipy.spatial import Delaunay from predicators import utils from predicators.spot_utils.perception.object_detection import detect_objects +from predicators.spot_utils.perception.object_perception import vlm_predicate_batch_classify, get_vlm_atom_combinations from predicators.spot_utils.perception.perception_structs import \ ObjectDetectionID, RGBDImageWithContext from predicators.spot_utils.perception.spot_cameras import capture_images @@ -23,7 +26,7 @@ DEFAULT_HAND_LOOK_FLOOR_POSE, get_allowed_map_regions, \ get_collision_geoms_for_nav, get_relative_se2_from_se3, \ sample_random_nearby_point_to_move, spot_pose_to_geom2d -from predicators.structs import State +from predicators.structs import State, VLMPredicate, Object, VLMGroundAtom def _find_objects_with_choreographed_moves( @@ -34,7 +37,9 @@ def _find_objects_with_choreographed_moves( relative_hand_moves: Optional[Sequence[math_helpers.SE3Pose]] = None, open_and_close_gripper: bool = True, allowed_regions: Optional[Collection[Delaunay]] = None, -) -> Tuple[Dict[ObjectDetectionID, math_helpers.SE3Pose], Dict[str, Any]]: + vlm_predicates: Optional[Set[VLMPredicate]] = None, + id2object: Optional[Dict[ObjectDetectionID, Object]] = None, +) -> Tuple[Dict[ObjectDetectionID, SE3Pose], Dict[str, Any], Dict[VLMGroundAtom, bool or None]]: """Helper for object search with hard-coded relative moves.""" if relative_hand_moves is not None: @@ -46,6 +51,10 @@ def _find_objects_with_choreographed_moves( # Save all RGBDs in case of failure so we can analyze them. all_rgbds: List[Dict[str, RGBDImageWithContext]] = [] + # Save VLMGroundAtoms from all poses + # NOTE: overwrite if the same atom is found; to improve later + all_vlm_atom_dict: Dict[VLMGroundAtom, bool or None] = defaultdict(lambda: None) + # Open the hand to mitigate possible occlusions. if open_and_close_gripper: open_gripper(robot) @@ -67,6 +76,20 @@ def _find_objects_with_choreographed_moves( print(f"Found objects: {set(all_detections)}") print(f"Remaining objects: {remaining_object_ids}") + # DEBUG Get VLM queries + Send request + if len(all_detections) > 0 and len(vlm_predicates) > 0: + objects = [id2object[id_] for id_ in all_detections] + vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) + vlm_atom_dict = vlm_predicate_batch_classify(vlm_atoms, rgbds, True) + # Update value if original is None while new is not None + for atom, result in vlm_atom_dict.items(): + if all_vlm_atom_dict[atom] is None and result is not None: + all_vlm_atom_dict[atom] = result + print(f"Calculated VLM atoms: {all_vlm_atom_dict}") + else: + # print("No VLM predicates or no objects found yet.") + pass + # Success, finish. if not remaining_object_ids: break @@ -94,7 +117,7 @@ def _find_objects_with_choreographed_moves( # Success, finish. remaining_object_ids = set(object_ids) - set(all_detections) if not remaining_object_ids: - return all_detections, all_artifacts + return all_detections, all_artifacts, all_vlm_atom_dict # Fail. Analyze the RGBDs if you want (by uncommenting here). # import imageio.v2 as iio @@ -115,7 +138,9 @@ def init_search_for_objects( num_spins: int = 8, relative_hand_moves: Optional[List[math_helpers.SE3Pose]] = None, allowed_regions: Optional[Collection[Delaunay]] = None, -) -> Tuple[Dict[ObjectDetectionID, math_helpers.SE3Pose], Dict[str, Any]]: + vlm_predicates: Optional[Set[VLMPredicate]] = None, + id2object: Optional[Dict[ObjectDetectionID, Object]] = None, +) -> Tuple[Dict[ObjectDetectionID, math_helpers.SE3Pose], Dict[str, Any], Dict[VLMGroundAtom, bool or None]]: """Spin around in place looking for objects. Raise a RuntimeError if an object can't be found after spinning. @@ -129,7 +154,10 @@ def init_search_for_objects( object_ids, base_moves, relative_hand_moves=relative_hand_moves, - allowed_regions=allowed_regions) + allowed_regions=allowed_regions, + vlm_predicates=vlm_predicates, + id2object=id2object, + ) def step_back_to_find_objects( From b4718f74e21bd627fd9b91df9db39c5def59cef2 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 16:49:55 -0400 Subject: [PATCH 33/71] batch VLM classifier eval: add VLM predicate fields to state+obs, build init obs that query all objects + all VLM predicates, update VLM ground atoms for visible objects, update VLM predicate classifiers --- predicators/envs/spot_env.py | 260 +++++++++++++++++------------------ 1 file changed, 129 insertions(+), 131 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 19c9b7fa50..3f008d0cb3 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -28,6 +28,8 @@ AprilTagObjectDetectionID, KnownStaticObjectDetectionID, \ LanguageObjectDetectionID, ObjectDetectionID, detect_objects, \ visualize_all_artifacts +from predicators.spot_utils.perception.object_perception import get_vlm_atom_combinations, \ + vlm_predicate_batch_classify from predicators.spot_utils.perception.object_specific_grasp_selection import \ brush_prompt, bucket_prompt, football_prompt, train_toy_prompt from predicators.spot_utils.perception.perception_structs import \ @@ -49,7 +51,7 @@ update_pbrspot_robot_conf, verify_estop from predicators.structs import Action, EnvironmentTask, GoalDescription, \ GroundAtom, LiftedAtom, Object, Observation, Predicate, \ - SpotActionExtraInfo, State, STRIPSOperator, Type, Variable + SpotActionExtraInfo, State, STRIPSOperator, Type, Variable, VLMPredicate, VLMGroundAtom ############################################################################### # Base Class # @@ -84,6 +86,9 @@ class _SpotObservation: # A placeholder until all predicates have classifiers nonpercept_atoms: Set[GroundAtom] nonpercept_predicates: Set[Predicate] + # VLM predicates and ground atoms + vlm_atom_dict: Optional[Dict[VLMGroundAtom, bool or None]] = None + vlm_predicates: Optional[Set[VLMPredicate]] = None class _PartialPerceptionState(State): @@ -126,7 +131,11 @@ def copy(self) -> State: } return _PartialPerceptionState(state_copy, simulator_state=sim_state_copy, - camera_images=self.camera_images) + camera_images=self.camera_images, + visible_objects=self.visible_objects, + vlm_atom_dict=self.vlm_atom_dict, + vlm_predicates=self.vlm_predicates, + ) def _create_dummy_predicate_classifier( @@ -550,7 +559,7 @@ def step(self, action: Action) -> Observation: while True: try: next_obs = self._build_realworld_observation( - next_nonpercept) + next_nonpercept, curr_obs=obs) break except RetryableRpcError as e: logging.warning("WARNING: the following retryable error " @@ -614,7 +623,7 @@ def goal_reached(self) -> bool: return self._current_task_goal_reached def _build_realworld_observation( - self, ground_atoms: Set[GroundAtom]) -> _SpotObservation: + self, nonpercept_atoms: Set[GroundAtom], curr_obs: Optional[_SpotObservation]) -> _SpotObservation: """Helper for building a new _SpotObservation() from real-robot data. This is an environment method because the nonpercept predicates @@ -736,12 +745,31 @@ def _build_realworld_observation( # Prepare the non-percepts. nonpercept_preds = self.predicates - self.percept_predicates - assert all(a.predicate in nonpercept_preds for a in ground_atoms) + assert all(a.predicate in nonpercept_preds for a in nonpercept_atoms) + + # Prepare the VLM predicates and ground atoms + if CFG.spot_vlm_eval_predicate: + vlm_predicates = _VLM_CLASSIFIER_PREDICATES + # Use currently visible objects to generate atom combinations + objects = list(all_objects_in_view.keys()) + vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) + vlm_atom_dict: Dict[VLMGroundAtom, bool or None] = vlm_predicate_batch_classify(vlm_atoms, rgbds, True) + # Update value if atoms in previous obs is None while new is not None + for atom, result in vlm_atom_dict.items(): + if curr_obs.vlm_atom_dict[atom] is None and result is not None: + vlm_atom_dict[atom] = result + + else: + vlm_predicates = set() + vlm_atom_dict = {} + + obs = _SpotObservation(rgbds, all_objects_in_view, objects_in_hand_view, objects_in_any_view_except_back, self._spot_object, gripper_open_percentage, - robot_pos, ground_atoms, nonpercept_preds) + robot_pos, nonpercept_atoms, nonpercept_preds, + vlm_atom_dict, vlm_predicates) return obs @@ -832,18 +860,28 @@ def _actively_construct_env_task(self) -> EnvironmentTask: # an initial observation. assert self._robot is not None assert self._localizer is not None - objects_in_view = self._actively_construct_initial_object_views() + + # TODO add logic for VLM predicates evaluation + objects_in_view, vlm_atom_dict = self._actively_construct_initial_object_views() rgbd_images = capture_images(self._robot, self._localizer) gripper_open_percentage = get_robot_gripper_open_percentage( self._robot) self._localizer.localize() robot_pos = self._localizer.get_last_robot_pose() + + # Update non-percept atoms and predicates nonpercept_atoms = self._get_initial_nonpercept_atoms() nonpercept_preds = self.predicates - self.percept_predicates assert all(a.predicate in nonpercept_preds for a in nonpercept_atoms) + + # TODO Prepare and query VLM for VLM predicates + # vlm_atoms = None + vlm_predicates = _VLM_CLASSIFIER_PREDICATES + obs = _SpotObservation(rgbd_images, objects_in_view, set(), set(), self._spot_object, gripper_open_percentage, - robot_pos, nonpercept_atoms, nonpercept_preds) + robot_pos, nonpercept_atoms, nonpercept_preds, + vlm_atom_dict, vlm_predicates) goal_description = self._generate_goal_description() task = EnvironmentTask(obs, goal_description) # Save the task for future use. @@ -976,25 +1014,28 @@ def _load_task_from_json(self, json_file: Path) -> EnvironmentTask: return EnvironmentTask(init_obs, goal) def _actively_construct_initial_object_views( - self) -> Dict[Object, math_helpers.SE3Pose]: + self) -> Tuple[ + Dict[Object, math_helpers.SE3Pose], + Dict[VLMGroundAtom, bool or None] + ]: assert self._robot is not None assert self._localizer is not None stow_arm(self._robot) go_home(self._robot, self._localizer) self._localizer.localize() detection_ids = self._detection_id_to_obj.keys() - detections = self._run_init_search_for_objects(set(detection_ids)) + detections, vlm_atom_dict = self._run_init_search_for_objects(set(detection_ids)) stow_arm(self._robot) obj_to_se3_pose = { self._detection_id_to_obj[det_id]: val for (det_id, val) in detections.items() } self._last_known_object_poses.update(obj_to_se3_pose) - return obj_to_se3_pose + return obj_to_se3_pose, vlm_atom_dict def _run_init_search_for_objects( self, detection_ids: Set[ObjectDetectionID] - ) -> Dict[ObjectDetectionID, math_helpers.SE3Pose]: + ) -> Tuple[Dict[ObjectDetectionID, math_helpers.SE3Pose], Dict[VLMGroundAtom, bool or None]]: """Have the hand look down from high up at first.""" assert self._robot is not None assert self._localizer is not None @@ -1004,11 +1045,15 @@ def _run_init_search_for_objects( rot=math_helpers.Quat.from_pitch( np.pi / 3)) move_hand_to_relative_pose(self._robot, hand_pose) - detections, artifacts = init_search_for_objects( + detections, artifacts, vlm_atom_dict = init_search_for_objects( self._robot, self._localizer, detection_ids, - allowed_regions=self._allowed_regions) + allowed_regions=self._allowed_regions, + # TODO input VLM predicates; to filter task-relevant ones + vlm_predicates=_VLM_CLASSIFIER_PREDICATES, + id2object=self._detection_id_to_obj, + ) if CFG.spot_render_perception_outputs: outdir = Path(CFG.spot_perception_outdir) time_str = time.strftime("%Y%m%d-%H%M%S") @@ -1016,7 +1061,7 @@ def _run_init_search_for_objects( no_detections_outfile = outdir / f"no_detections_{time_str}.png" visualize_all_artifacts(artifacts, detections_outfile, no_detections_outfile) - return detections + return detections, vlm_atom_dict @property @abc.abstractmethod @@ -1036,64 +1081,7 @@ def _generate_goal_description(self) -> GoalDescription: # VLM Predicate Evaluation Related # ############################################################################### -# Initialize VLM -vlm = OpenAIVLM(model_name="gpt-4-turbo", detail="auto") - -# Engineer the prompt for VLM -vlm_predicate_eval_prompt_prefix = """ -Your goal is to answer questions related to object relationships in the -given image(s). -We will use following predicate-style descriptions to ask questions: - Inside(object1, container) - Blocking(object1, object2) - On(object, surface) - -Examples: -Does this predicate hold in the following image? -Inside(apple, bowl) -Answer (in a single word): Yes/No - -Actual question: -Does this predicate hold in the following image? -{question} -Answer (in a single word): -""" - -# Provide some visual examples when needed -vlm_predicate_eval_prompt_example = "" -# TODO: Next, try include visual hints via segmentation ("Set of Masks") - - -def vlm_predicate_classify(question: str, state: State) -> bool: - """Use VLM to evaluate (classify) a predicate in a given state.""" - full_prompt = vlm_predicate_eval_prompt_prefix.format(question=question) - images_dict: Dict[str, RGBDImageWithContext] = state.camera_images - images = [ - PIL.Image.fromarray(v.rotated_rgb) for _, v in images_dict.items() - ] - - logging.info(f"VLM predicate evaluation for: {question}") - logging.info(f"Prompt: {full_prompt}") - - vlm_responses = vlm.sample_completions( - prompt=full_prompt, - imgs=images, - temperature=0.2, - seed=int(time.time()), - num_completions=1, - ) - logging.info(f"VLM response 0: {vlm_responses[0]}") - - vlm_response = vlm_responses[0].strip().lower() - if vlm_response == "yes": - return True - elif vlm_response == "no": - return False - else: - logging.error( - f"VLM response not understood: {vlm_response}. Treat as False.") - return False - +# TODO: move to a separate file ############################################################################### # Shared Types, Predicates, Operators # @@ -1185,11 +1173,12 @@ def _on_classifier(state: State, objects: Sequence[Object]) -> bool: # Call VLM to evaluate predicate value elif CFG.spot_vlm_eval_predicate and currently_visible: - predicate_str = f""" - On({obj_on}, {obj_surface}) - (Whether {obj_on} is on {obj_surface} in the image?) - """ - return vlm_predicate_classify(predicate_str, state) + # predicate_str = f""" + # On({obj_on}, {obj_surface}) + # (Whether {obj_on} is on {obj_surface} in the image?) + # """ + # return vlm_predicate_classify(predicate_str, state) + raise RuntimeError("VLM predicate classifier should be evaluated in batch!") else: # Check that the bottom of the object is close to the top of the surface. @@ -1218,42 +1207,42 @@ def _top_above_classifier(state: State, objects: Sequence[Object]) -> bool: def _inside_classifier(state: State, objects: Sequence[Object]) -> bool: obj_in, obj_container = objects - currently_visible = all([o in state.visible_objects for o in objects]) - # If object not all visible and choose to use VLM, - # then use predicate values of previous time step - if CFG.spot_vlm_eval_predicate and not currently_visible: - # TODO: add all previous atoms to the state - raise NotImplementedError - - # Call VLM to evaluate predicate value - elif CFG.spot_vlm_eval_predicate and currently_visible: - predicate_str = f""" - Inside({obj_in}, {obj_container}) - (Whether {obj_in} is inside {obj_container} in the image?) - """ - return vlm_predicate_classify(predicate_str, state) - - else: - if not _object_in_xy_classifier( - state, obj_in, obj_container, buffer=_INSIDE_SURFACE_BUFFER): - return False + # currently_visible = all([o in state.visible_objects for o in objects]) + # # If object not all visible and choose to use VLM, + # # then use predicate values of previous time step + # if CFG.spot_vlm_eval_predicate and not currently_visible: + # # TODO: add all previous atoms to the state + # raise NotImplementedError + # + # # Call VLM to evaluate predicate value + # elif CFG.spot_vlm_eval_predicate and currently_visible: + # predicate_str = f""" + # Inside({obj_in}, {obj_container}) + # (Whether {obj_in} is inside {obj_container} in the image?) + # """ + # return vlm_predicate_classify(predicate_str, state) + # + # else: + if not _object_in_xy_classifier( + state, obj_in, obj_container, buffer=_INSIDE_SURFACE_BUFFER): + return False - obj_z = state.get(obj_in, "z") - obj_half_height = state.get(obj_in, "height") / 2 - obj_bottom = obj_z - obj_half_height - obj_top = obj_z + obj_half_height + obj_z = state.get(obj_in, "z") + obj_half_height = state.get(obj_in, "height") / 2 + obj_bottom = obj_z - obj_half_height + obj_top = obj_z + obj_half_height - container_z = state.get(obj_container, "z") - container_half_height = state.get(obj_container, "height") / 2 - container_bottom = container_z - container_half_height - container_top = container_z + container_half_height + container_z = state.get(obj_container, "z") + container_half_height = state.get(obj_container, "height") / 2 + container_bottom = container_z - container_half_height + container_top = container_z + container_half_height - # Check that the bottom is "above" the bottom of the container. - if obj_bottom < container_bottom - _INSIDE_Z_THRESHOLD: - return False + # Check that the bottom is "above" the bottom of the container. + if obj_bottom < container_bottom - _INSIDE_Z_THRESHOLD: + return False - # Check that the top is "below" the top of the container. - return obj_top < container_top + _INSIDE_Z_THRESHOLD + # Check that the top is "below" the top of the container. + return obj_top < container_top + _INSIDE_Z_THRESHOLD def _not_inside_any_container_classifier(state: State, @@ -1345,20 +1334,20 @@ def _blocking_classifier(state: State, objects: Sequence[Object]) -> bool: if blocker_obj == blocked_obj: return False - currently_visible = all([o in state.visible_objects for o in objects]) - # If object not all visible and choose to use VLM, - # then use predicate values of previous time step - if CFG.spot_vlm_eval_predicate and not currently_visible: - # TODO: add all previous atoms to the state - raise NotImplementedError - - # Call VLM to evaluate predicate value - elif CFG.spot_vlm_eval_predicate and currently_visible: - predicate_str = f""" - (Whether {blocker_obj} is blocking {blocked_obj} for further manipulation in the image?) - Blocking({blocker_obj}, {blocked_obj}) - """ - return vlm_predicate_classify(predicate_str, state) + # currently_visible = all([o in state.visible_objects for o in objects]) + # # If object not all visible and choose to use VLM, + # # then use predicate values of previous time step + # if CFG.spot_vlm_eval_predicate and not currently_visible: + # # TODO: add all previous atoms to the state + # raise NotImplementedError + # + # # Call VLM to evaluate predicate value + # elif CFG.spot_vlm_eval_predicate and currently_visible: + # predicate_str = f""" + # (Whether {blocker_obj} is blocking {blocked_obj} for further manipulation in the image?) + # Blocking({blocker_obj}, {blocked_obj}) + # """ + # return vlm_predicate_classify(predicate_str, state) # Only consider draggable (non-placeable, movable) objects to be blockers. if not blocker_obj.is_instance(_movable_object_type): @@ -1513,8 +1502,8 @@ def _get_sweeping_surface_for_container(container: Object, _NEq = Predicate("NEq", [_base_object_type, _base_object_type], _neq_classifier) -_On = Predicate("On", [_movable_object_type, _base_object_type], - _on_classifier) +# _On = Predicate("On", [_movable_object_type, _base_object_type], +# _on_classifier) _TopAbove = Predicate("TopAbove", [_base_object_type, _base_object_type], _top_above_classifier) _Inside = Predicate("Inside", [_movable_object_type, _container_type], @@ -1559,6 +1548,17 @@ def _get_sweeping_surface_for_container(container: Object, _IsSemanticallyGreaterThan = Predicate( "IsSemanticallyGreaterThan", [_base_object_type, _base_object_type], _is_semantically_greater_than_classifier) + +# if CFG.spot_vlm_eval_predicate: +# _On = VLMPredicate("On", [_movable_object_type, _base_object_type], +# _on_classifier) +# # _Inside = VLMPredicate("Inside", [_movable_object_type, _container_type], +# # _inside_classifier) + +_On = VLMPredicate("On", [_movable_object_type, _base_object_type], + _on_classifier) + +# NOTE: Define all regular or VLM predicates above _ALL_PREDICATES = { _NEq, _On, _TopAbove, _Inside, _NotInsideAnyContainer, _FitsInXY, _HandEmpty, _Holding, _NotHolding, _InHandView, _InView, _Reachable, @@ -1567,13 +1567,12 @@ def _get_sweeping_surface_for_container(container: Object, _IsSemanticallyGreaterThan } _NONPERCEPT_PREDICATES: Set[Predicate] = set() -# NOTE: We maintain a list of predicates that we check via + # NOTE: In the future, we may include an attribute to denote whether a predicate # is VLM perceptible or not. # NOTE: candidates: on, inside, door opened, blocking, not blocked, ... -_VLM_EVAL_PREDICATES: { - _On, - _Inside, +_VLM_CLASSIFIER_PREDICATES: Set[VLMPredicate] = { + p for p in _ALL_PREDICATES if isinstance(p, VLMPredicate) } @@ -2155,7 +2154,6 @@ def _dry_simulate_place_on_top( last_obs: _SpotObservation, held_obj: Object, target_surface: Object, place_offset: math_helpers.Vec3, nonpercept_atoms: Set[GroundAtom]) -> _SpotObservation: - # Initialize values based on the last observation. objects_in_view = last_obs.objects_in_view.copy() objects_in_hand_view = set(last_obs.objects_in_hand_view) From 3ca9032e6a0cdd5d5d8885e7b6ec533f3dbc6f86 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 16:51:45 -0400 Subject: [PATCH 34/71] formatting --- predicators/envs/spot_env.py | 72 ++++++++++--------- predicators/perception/spot_perceiver.py | 15 ++-- .../perception/object_perception.py | 33 ++++++--- .../spot_utils/skills/spot_find_objects.py | 20 ++++-- predicators/structs.py | 27 ++++--- 5 files changed, 99 insertions(+), 68 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 3f008d0cb3..41150e71a3 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -9,10 +9,10 @@ from typing import Callable, ClassVar, Collection, Dict, Iterator, List, \ Optional, Sequence, Set, Tuple -import PIL.Image import matplotlib import numpy as np import pbrspot +import PIL.Image from bosdyn.client import RetryableRpcError, create_standard_sdk, math_helpers from bosdyn.client.lease import LeaseClient, LeaseKeepAlive from bosdyn.client.sdk import Robot @@ -28,8 +28,8 @@ AprilTagObjectDetectionID, KnownStaticObjectDetectionID, \ LanguageObjectDetectionID, ObjectDetectionID, detect_objects, \ visualize_all_artifacts -from predicators.spot_utils.perception.object_perception import get_vlm_atom_combinations, \ - vlm_predicate_batch_classify +from predicators.spot_utils.perception.object_perception import \ + get_vlm_atom_combinations, vlm_predicate_batch_classify from predicators.spot_utils.perception.object_specific_grasp_selection import \ brush_prompt, bucket_prompt, football_prompt, train_toy_prompt from predicators.spot_utils.perception.perception_structs import \ @@ -51,7 +51,8 @@ update_pbrspot_robot_conf, verify_estop from predicators.structs import Action, EnvironmentTask, GoalDescription, \ GroundAtom, LiftedAtom, Object, Observation, Predicate, \ - SpotActionExtraInfo, State, STRIPSOperator, Type, Variable, VLMPredicate, VLMGroundAtom + SpotActionExtraInfo, State, STRIPSOperator, Type, Variable, \ + VLMGroundAtom, VLMPredicate ############################################################################### # Base Class # @@ -129,13 +130,14 @@ def copy(self) -> State: "predicates": self._simulator_state_predicates.copy(), "atoms": self._simulator_state_atoms.copy() } - return _PartialPerceptionState(state_copy, - simulator_state=sim_state_copy, - camera_images=self.camera_images, - visible_objects=self.visible_objects, - vlm_atom_dict=self.vlm_atom_dict, - vlm_predicates=self.vlm_predicates, - ) + return _PartialPerceptionState( + state_copy, + simulator_state=sim_state_copy, + camera_images=self.camera_images, + visible_objects=self.visible_objects, + vlm_atom_dict=self.vlm_atom_dict, + vlm_predicates=self.vlm_predicates, + ) def _create_dummy_predicate_classifier( @@ -312,7 +314,7 @@ def percept_predicates(self) -> Set[Predicate]: def action_space(self) -> Box: # The action space is effectively empty because only the extra info # part of actions are used. - return Box(0, 1, (0,)) + return Box(0, 1, (0, )) @abc.abstractmethod def _get_dry_task(self, train_or_test: str, @@ -350,7 +352,7 @@ def _get_next_dry_observation( nonpercept_atoms) if action_name in [ - "MoveToReachObject", "MoveToReadySweep", "MoveToBodyViewObject" + "MoveToReachObject", "MoveToReadySweep", "MoveToBodyViewObject" ]: robot_rel_se2_pose = action_args[1] return _dry_simulate_move_to_reach_obj(obs, robot_rel_se2_pose, @@ -623,7 +625,8 @@ def goal_reached(self) -> bool: return self._current_task_goal_reached def _build_realworld_observation( - self, nonpercept_atoms: Set[GroundAtom], curr_obs: Optional[_SpotObservation]) -> _SpotObservation: + self, nonpercept_atoms: Set[GroundAtom], + curr_obs: Optional[_SpotObservation]) -> _SpotObservation: """Helper for building a new _SpotObservation() from real-robot data. This is an environment method because the nonpercept predicates @@ -753,7 +756,9 @@ def _build_realworld_observation( # Use currently visible objects to generate atom combinations objects = list(all_objects_in_view.keys()) vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) - vlm_atom_dict: Dict[VLMGroundAtom, bool or None] = vlm_predicate_batch_classify(vlm_atoms, rgbds, True) + vlm_atom_dict: Dict[VLMGroundAtom, + bool or None] = vlm_predicate_batch_classify( + vlm_atoms, rgbds, True) # Update value if atoms in previous obs is None while new is not None for atom, result in vlm_atom_dict.items(): if curr_obs.vlm_atom_dict[atom] is None and result is not None: @@ -763,7 +768,6 @@ def _build_realworld_observation( vlm_predicates = set() vlm_atom_dict = {} - obs = _SpotObservation(rgbds, all_objects_in_view, objects_in_hand_view, objects_in_any_view_except_back, @@ -862,7 +866,8 @@ def _actively_construct_env_task(self) -> EnvironmentTask: assert self._localizer is not None # TODO add logic for VLM predicates evaluation - objects_in_view, vlm_atom_dict = self._actively_construct_initial_object_views() + objects_in_view, vlm_atom_dict = self._actively_construct_initial_object_views( + ) rgbd_images = capture_images(self._robot, self._localizer) gripper_open_percentage = get_robot_gripper_open_percentage( self._robot) @@ -1014,17 +1019,17 @@ def _load_task_from_json(self, json_file: Path) -> EnvironmentTask: return EnvironmentTask(init_obs, goal) def _actively_construct_initial_object_views( - self) -> Tuple[ - Dict[Object, math_helpers.SE3Pose], - Dict[VLMGroundAtom, bool or None] - ]: + self + ) -> Tuple[Dict[Object, math_helpers.SE3Pose], Dict[VLMGroundAtom, + bool or None]]: assert self._robot is not None assert self._localizer is not None stow_arm(self._robot) go_home(self._robot, self._localizer) self._localizer.localize() detection_ids = self._detection_id_to_obj.keys() - detections, vlm_atom_dict = self._run_init_search_for_objects(set(detection_ids)) + detections, vlm_atom_dict = self._run_init_search_for_objects( + set(detection_ids)) stow_arm(self._robot) obj_to_se3_pose = { self._detection_id_to_obj[det_id]: val @@ -1034,8 +1039,9 @@ def _actively_construct_initial_object_views( return obj_to_se3_pose, vlm_atom_dict def _run_init_search_for_objects( - self, detection_ids: Set[ObjectDetectionID] - ) -> Tuple[Dict[ObjectDetectionID, math_helpers.SE3Pose], Dict[VLMGroundAtom, bool or None]]: + self, detection_ids: Set[ObjectDetectionID] + ) -> Tuple[Dict[ObjectDetectionID, math_helpers.SE3Pose], Dict[ + VLMGroundAtom, bool or None]]: """Have the hand look down from high up at first.""" assert self._robot is not None assert self._localizer is not None @@ -1178,7 +1184,8 @@ def _on_classifier(state: State, objects: Sequence[Object]) -> bool: # (Whether {obj_on} is on {obj_surface} in the image?) # """ # return vlm_predicate_classify(predicate_str, state) - raise RuntimeError("VLM predicate classifier should be evaluated in batch!") + raise RuntimeError( + "VLM predicate classifier should be evaluated in batch!") else: # Check that the bottom of the object is close to the top of the surface. @@ -1291,8 +1298,8 @@ def in_general_view_classifier(state: State, def _obj_reachable_from_spot_pose(spot_pose: math_helpers.SE3Pose, obj_position: math_helpers.Vec3) -> bool: is_xy_near = np.sqrt( - (spot_pose.x - obj_position.x) ** 2 + - (spot_pose.y - obj_position.y) ** 2) <= _REACHABLE_THRESHOLD + (spot_pose.x - obj_position.x)**2 + + (spot_pose.y - obj_position.y)**2) <= _REACHABLE_THRESHOLD # Compute angle between spot's forward direction and the line from # spot to the object. @@ -1433,8 +1440,8 @@ def _container_adjacent_to_surface_for_sweeping(container: Object, container_x = state.get(container, "x") container_y = state.get(container, "y") - dist = np.sqrt((expected_x - container_x) ** 2 + - (expected_y - container_y) ** 2) + dist = np.sqrt((expected_x - container_x)**2 + + (expected_y - container_y)**2) return dist <= _CONTAINER_SWEEP_READY_BUFFER @@ -1556,7 +1563,7 @@ def _get_sweeping_surface_for_container(container: Object, # # _inside_classifier) _On = VLMPredicate("On", [_movable_object_type, _base_object_type], - _on_classifier) + _on_classifier) # NOTE: Define all regular or VLM predicates above _ALL_PREDICATES = { @@ -1572,7 +1579,8 @@ def _get_sweeping_surface_for_container(container: Object, # is VLM perceptible or not. # NOTE: candidates: on, inside, door opened, blocking, not blocked, ... _VLM_CLASSIFIER_PREDICATES: Set[VLMPredicate] = { - p for p in _ALL_PREDICATES if isinstance(p, VLMPredicate) + p + for p in _ALL_PREDICATES if isinstance(p, VLMPredicate) } @@ -2393,7 +2401,7 @@ def _dry_simulate_sweep_into_container( x = container_pose.x + dx y = container_pose.y + dy z = container_pose.z - dist_to_container = (dx ** 2 + dy ** 2) ** 0.5 + dist_to_container = (dx**2 + dy**2)**0.5 assert dist_to_container > (container_radius + _INSIDE_SURFACE_BUFFER) diff --git a/predicators/perception/spot_perceiver.py b/predicators/perception/spot_perceiver.py index 40682e571d..e28b8d9970 100644 --- a/predicators/perception/spot_perceiver.py +++ b/predicators/perception/spot_perceiver.py @@ -293,13 +293,14 @@ def _create_state(self) -> State: camera_images = self._camera_images if CFG.spot_vlm_eval_predicate else None # Now finish the state. - state = _PartialPerceptionState(percept_state.data, - simulator_state=simulator_state, - camera_images=camera_images, - visible_objects=self._objects_in_view, - vlm_atom_dict=self._vlm_atom_dict, - vlm_predicates=self._vlm_predicates, - ) + state = _PartialPerceptionState( + percept_state.data, + simulator_state=simulator_state, + camera_images=camera_images, + visible_objects=self._objects_in_view, + vlm_atom_dict=self._vlm_atom_dict, + vlm_predicates=self._vlm_predicates, + ) # DEBUG - look into dataclass field init - why warning return state diff --git a/predicators/spot_utils/perception/object_perception.py b/predicators/spot_utils/perception/object_perception.py index bcd3cf3d1f..f95194caa9 100644 --- a/predicators/spot_utils/perception/object_perception.py +++ b/predicators/spot_utils/perception/object_perception.py @@ -2,13 +2,14 @@ import logging import time -from typing import Dict, List, Set, Sequence +from typing import Dict, List, Sequence, Set import PIL.Image from predicators.pretrained_model_interface import OpenAIVLM -from predicators.spot_utils.perception.perception_structs import RGBDImageWithContext -from predicators.structs import State, VLMGroundAtom, Object, VLMPredicate +from predicators.spot_utils.perception.perception_structs import \ + RGBDImageWithContext +from predicators.structs import Object, State, VLMGroundAtom, VLMPredicate from predicators.utils import get_object_combinations ############################################################################### @@ -109,10 +110,14 @@ def vlm_predicate_classify(question: str, state: State) -> bool | None: return None -def vlm_predicate_batch_query(queries: List[str], images: Dict[str, RGBDImageWithContext]) -> List[bool]: - """Use queries generated from VLM predicates to evaluate them via VLM in batch. +def vlm_predicate_batch_query( + queries: List[str], images: Dict[str, + RGBDImageWithContext]) -> List[bool]: + """Use queries generated from VLM predicates to evaluate them via VLM in + batch. - The VLM takes a list of queries and images in current observation to evaluate them. + The VLM takes a list of queries and images in current observation to + evaluate them. """ # Assemble the full prompt @@ -139,20 +144,25 @@ def vlm_predicate_batch_query(queries: List[str], images: Dict[str, RGBDImageWit responses = vlm_responses[0].strip().lower().split('\n') results = [] for i, r in enumerate(responses): - assert r in ['yes', 'no', 'unknown'], f"Invalid response in line {i}: {r}" + assert r in ['yes', 'no', + 'unknown'], f"Invalid response in line {i}: {r}" if r == 'yes': results.append(True) elif r == 'no': results.append(False) else: results.append(None) - assert len(results) == len(queries), "Number of responses should match queries." + assert len(results) == len( + queries), "Number of responses should match queries." return results -def vlm_predicate_batch_classify(atoms: Set[VLMGroundAtom], images: Dict[str, RGBDImageWithContext], - get_dict: bool = True) -> Dict[VLMGroundAtom, bool] | Set[VLMGroundAtom]: +def vlm_predicate_batch_classify( + atoms: Set[VLMGroundAtom], + images: Dict[str, RGBDImageWithContext], + get_dict: bool = True +) -> Dict[VLMGroundAtom, bool] | Set[VLMGroundAtom]: """Use VLM to evaluate a set of atoms in a given state.""" # Get the queries for the atoms queries = [atom.get_query_str() for atom in atoms] @@ -176,7 +186,8 @@ def vlm_predicate_batch_classify(atoms: Set[VLMGroundAtom], images: Dict[str, RG return hold_atoms -def get_vlm_atom_combinations(objects: Sequence[Object], preds: Set[VLMPredicate]) -> Set[VLMGroundAtom]: +def get_vlm_atom_combinations(objects: Sequence[Object], + preds: Set[VLMPredicate]) -> Set[VLMGroundAtom]: atoms = set() for pred in preds: for choice in get_object_combinations(objects, pred.types): diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index 7df9a69ad5..b6467286c7 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -2,7 +2,8 @@ import logging import time from collections import defaultdict -from typing import Any, Collection, Dict, List, Optional, Sequence, Tuple, Set, Callable +from typing import Any, Callable, Collection, Dict, List, Optional, Sequence, \ + Set, Tuple import numpy as np from bosdyn.client import math_helpers @@ -13,7 +14,8 @@ from predicators import utils from predicators.spot_utils.perception.object_detection import detect_objects -from predicators.spot_utils.perception.object_perception import vlm_predicate_batch_classify, get_vlm_atom_combinations +from predicators.spot_utils.perception.object_perception import \ + get_vlm_atom_combinations, vlm_predicate_batch_classify from predicators.spot_utils.perception.perception_structs import \ ObjectDetectionID, RGBDImageWithContext from predicators.spot_utils.perception.spot_cameras import capture_images @@ -26,7 +28,7 @@ DEFAULT_HAND_LOOK_FLOOR_POSE, get_allowed_map_regions, \ get_collision_geoms_for_nav, get_relative_se2_from_se3, \ sample_random_nearby_point_to_move, spot_pose_to_geom2d -from predicators.structs import State, VLMPredicate, Object, VLMGroundAtom +from predicators.structs import Object, State, VLMGroundAtom, VLMPredicate def _find_objects_with_choreographed_moves( @@ -39,7 +41,8 @@ def _find_objects_with_choreographed_moves( allowed_regions: Optional[Collection[Delaunay]] = None, vlm_predicates: Optional[Set[VLMPredicate]] = None, id2object: Optional[Dict[ObjectDetectionID, Object]] = None, -) -> Tuple[Dict[ObjectDetectionID, SE3Pose], Dict[str, Any], Dict[VLMGroundAtom, bool or None]]: +) -> Tuple[Dict[ObjectDetectionID, SE3Pose], Dict[str, Any], Dict[ + VLMGroundAtom, bool or None]]: """Helper for object search with hard-coded relative moves.""" if relative_hand_moves is not None: @@ -53,7 +56,8 @@ def _find_objects_with_choreographed_moves( # Save VLMGroundAtoms from all poses # NOTE: overwrite if the same atom is found; to improve later - all_vlm_atom_dict: Dict[VLMGroundAtom, bool or None] = defaultdict(lambda: None) + all_vlm_atom_dict: Dict[VLMGroundAtom, + bool or None] = defaultdict(lambda: None) # Open the hand to mitigate possible occlusions. if open_and_close_gripper: @@ -80,7 +84,8 @@ def _find_objects_with_choreographed_moves( if len(all_detections) > 0 and len(vlm_predicates) > 0: objects = [id2object[id_] for id_ in all_detections] vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) - vlm_atom_dict = vlm_predicate_batch_classify(vlm_atoms, rgbds, True) + vlm_atom_dict = vlm_predicate_batch_classify( + vlm_atoms, rgbds, True) # Update value if original is None while new is not None for atom, result in vlm_atom_dict.items(): if all_vlm_atom_dict[atom] is None and result is not None: @@ -140,7 +145,8 @@ def init_search_for_objects( allowed_regions: Optional[Collection[Delaunay]] = None, vlm_predicates: Optional[Set[VLMPredicate]] = None, id2object: Optional[Dict[ObjectDetectionID, Object]] = None, -) -> Tuple[Dict[ObjectDetectionID, math_helpers.SE3Pose], Dict[str, Any], Dict[VLMGroundAtom, bool or None]]: +) -> Tuple[Dict[ObjectDetectionID, math_helpers.SE3Pose], Dict[str, Any], Dict[ + VLMGroundAtom, bool or None]]: """Spin around in place looking for objects. Raise a RuntimeError if an object can't be found after spinning. diff --git a/predicators/structs.py b/predicators/structs.py index f2a01dcef4..ad0ea8b607 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -320,16 +320,18 @@ class VLMPredicate(Predicate): """Struct defining a predicate (a lifted classifier over states) that uses a VLM for evaluation. - It overrides the `holds` method, which only return the stored predicate - value in the State. Instead, it supports a query method that generates VLM - query, where all VLM predicates will be evaluated at once. + It overrides the `holds` method, which only return the stored + predicate value in the State. Instead, it supports a query method + that generates VLM query, where all VLM predicates will be evaluated + at once. """ def get_query(self, objects: Sequence[Object]) -> str: """Get a query string for this predicate. - Instead of directly evaluating the predicate, we will use the VLM to - evaluate all VLM predicate classifiers in a batched manner. + Instead of directly evaluating the predicate, we will use the + VLM to evaluate all VLM predicate classifiers in a batched + manner. """ self.pddl_str() @@ -462,9 +464,10 @@ class VLMGroundAtom(GroundAtom): """Struct defining a ground atom (a predicate applied to objects) that uses a VLM for evaluation. - It overrides the `holds` method, which only return the stored predicate - value in the State. Instead, it supports a query method that generates VLM - query, where all VLM predicates will be evaluated at once. + It overrides the `holds` method, which only return the stored + predicate value in the State. Instead, it supports a query method + that generates VLM query, where all VLM predicates will be evaluated + at once. """ # NOTE: This subclasses GroundAtom to support VLM predicates and classifiers @@ -473,11 +476,13 @@ class VLMGroundAtom(GroundAtom): def get_query_str(self, without_type: bool = False) -> str: """Get a query string for this ground atom. - Instead of directly evaluating the ground atom, we will use the VLM to - evaluate all VLM predicate classifiers in a batched manner. + Instead of directly evaluating the ground atom, we will use the + VLM to evaluate all VLM predicate classifiers in a batched + manner. """ if without_type: - string = self.predicate.name + "(" + ", ".join(o.name for o in self.objects) + ")" + string = self.predicate.name + "(" + ", ".join( + o.name for o in self.objects) + ")" else: string = str(self) return string From 1ab3d36dac8275ff6c38848b32c3f2497e6382ac Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 16:55:18 -0400 Subject: [PATCH 35/71] remove some comments --- .../spot_utils/skills/spot_find_objects.py | 4 ++-- predicators/structs.py | 18 +++--------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index b6467286c7..1d710d85bd 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -80,7 +80,7 @@ def _find_objects_with_choreographed_moves( print(f"Found objects: {set(all_detections)}") print(f"Remaining objects: {remaining_object_ids}") - # DEBUG Get VLM queries + Send request + # Get VLM queries + Send request if len(all_detections) > 0 and len(vlm_predicates) > 0: objects = [id2object[id_] for id_ in all_detections] vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) @@ -92,7 +92,7 @@ def _find_objects_with_choreographed_moves( all_vlm_atom_dict[atom] = result print(f"Calculated VLM atoms: {all_vlm_atom_dict}") else: - # print("No VLM predicates or no objects found yet.") + # No VLM predicates or no objects found yet pass # Success, finish. diff --git a/predicators/structs.py b/predicators/structs.py index ad0ea8b607..29350addbd 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -119,8 +119,7 @@ class State: simulator_state: Optional[Any] = None # Store additional fields for VLM predicate classifiers - # NOTE: adding in Spot ev subclass doesn't work; may need fix - # prev_atoms: Optional[Dict[str, bool]] = None + # NOTE: adding in Spot subclass doesn't work; may need fix vlm_atom_dict: Optional[Dict[VLMGroundAtom, bool or None]] = None vlm_predicates: Optional[Collection[Predicate]] = None visible_objects: Optional[Any] = None @@ -326,15 +325,6 @@ class VLMPredicate(Predicate): at once. """ - def get_query(self, objects: Sequence[Object]) -> str: - """Get a query string for this predicate. - - Instead of directly evaluating the predicate, we will use the - VLM to evaluate all VLM predicate classifiers in a batched - manner. - """ - self.pddl_str() - def holds(self, state: State, objects: Sequence[Object]) -> bool: """Public method for getting predicate value. @@ -345,10 +335,8 @@ def holds(self, state: State, objects: Sequence[Object]) -> bool: assert isinstance(obj, Object) assert obj.is_instance(pred_type) - # TODO get predicate values from State - # TODO store a dict, from str to bool; but it should be str of GroundAtom or Predicate? - # return state.prev_atoms[str(self)] - # return self._classifier(state, objects) + # Get VLM predicate values from State + # It is stored in a dictionary of VLMGroundAtom -> bool return state.vlm_atom_dict[VLMGroundAtom(self, objects)] From 11f91ffbe4f8688cdd1cef7ff224f3dda7966893 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 17:06:32 -0400 Subject: [PATCH 36/71] remove some comments --- predicators/structs.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/predicators/structs.py b/predicators/structs.py index 29350addbd..e713af9c94 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -481,9 +481,7 @@ def holds(self, state: State) -> bool: Retrieve GroundAtom value from State directly. """ assert isinstance(self.predicate, VLMPredicate) - # TODO get predicate values from State return state.vlm_atom_dict[self] - # return self.predicate.holds(state, self.objects) @dataclass(frozen=True, eq=False) From c98e2feb8af91903ca38b5ce6458d154b244cb11 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 18:22:23 -0400 Subject: [PATCH 37/71] fix, add tenacity --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 4a00530c0a..bd3b2b499a 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ "google-generativeai", "ImageHash", "rich", + "tenacity", ], include_package_data=True, extras_require={ From 4973f1f08719ee803dc020d120b8214c28ad7098 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 18:33:33 -0400 Subject: [PATCH 38/71] fix structs --- predicators/structs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/predicators/structs.py b/predicators/structs.py index e713af9c94..0141938c36 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -337,6 +337,7 @@ def holds(self, state: State, objects: Sequence[Object]) -> bool: # Get VLM predicate values from State # It is stored in a dictionary of VLMGroundAtom -> bool + assert state.vlm_atom_dict is not None return state.vlm_atom_dict[VLMGroundAtom(self, objects)] @@ -481,6 +482,7 @@ def holds(self, state: State) -> bool: Retrieve GroundAtom value from State directly. """ assert isinstance(self.predicate, VLMPredicate) + assert state.vlm_atom_dict is not None return state.vlm_atom_dict[self] From 26239e8d0acfbec894ff6f8d377ebcf56fb47a99 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 18:43:03 -0400 Subject: [PATCH 39/71] more fix --- predicators/pretrained_model_interface.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py index 08dcdc6e71..3bbbe9786d 100644 --- a/predicators/pretrained_model_interface.py +++ b/predicators/pretrained_model_interface.py @@ -10,7 +10,7 @@ import os import time from io import BytesIO -from typing import List, Optional +from typing import List, Optional, Dict import cv2 import google @@ -265,7 +265,7 @@ def __init__(self, model_name: str = "gpt-4-turbo", detail: str = "auto"): self.detail = detail self.set_openai_key() - def set_openai_key(self, key: Optional[str] = None): + def set_openai_key(self, key: Optional[str] = None) -> None: """Set the OpenAI API key.""" if key is None: assert "OPENAI_API_KEY" in os.environ @@ -277,7 +277,7 @@ def prepare_vision_messages(self, prefix: Optional[str] = None, suffix: Optional[str] = None, image_size: Optional[int] = 512, - detail: str = "auto"): + detail: str = "auto") -> List[Dict[str, str]]: """Prepare text and image messages for the OpenAI API.""" content = [] @@ -299,8 +299,8 @@ def prepare_vision_messages(self, # Convert the image to PNG format and encode it in base64 buffer = BytesIO() img_resized.save(buffer, format="PNG") - buffer = buffer.getvalue() - frame = base64.b64encode(buffer).decode("utf-8") + buffer_bytes = buffer.getvalue() + frame = base64.b64encode(buffer_bytes).decode("utf-8") content.append({ "image_url": { @@ -323,7 +323,7 @@ def call_openai_api(self, seed: Optional[int] = None, max_tokens: int = 32, temperature: float = 0.2, - verbose: bool = False): + verbose: bool = False) -> str: """Make an API call to OpenAI.""" client = openai.OpenAI() completion = client.chat.completions.create( @@ -350,9 +350,10 @@ def _sample_completions( seed: int, stop_token: Optional[str] = None, num_completions: int = 1, - max_tokens=512, + max_tokens: int = 512, ) -> List[str]: """Query the model and get responses.""" + assert imgs is not None messages = self.prepare_vision_messages(prefix=prompt, images=imgs, detail="auto") From e78e3428e19376bb5da2ee3b3c897598836e7219 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 22:10:58 -0400 Subject: [PATCH 40/71] update --- predicators/pretrained_model_interface.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py index 3bbbe9786d..7b8834c4c0 100644 --- a/predicators/pretrained_model_interface.py +++ b/predicators/pretrained_model_interface.py @@ -263,14 +263,8 @@ def __init__(self, model_name: str = "gpt-4-turbo", detail: str = "auto"): """Initialize with a specific model name.""" self.model_name = model_name self.detail = detail - self.set_openai_key() - - def set_openai_key(self, key: Optional[str] = None) -> None: - """Set the OpenAI API key.""" - if key is None: - assert "OPENAI_API_KEY" in os.environ - key = os.environ["OPENAI_API_KEY"] - openai.api_key = key + assert "OPENAI_API_KEY" in os.environ + openai.api_key = os.getenv("OPENAI_API_KEY") def prepare_vision_messages(self, images: List[PIL.Image.Image], From ce43a768c18f779aad6678f7466cf8446383ea18 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Wed, 8 May 2024 23:02:40 -0400 Subject: [PATCH 41/71] some clean --- predicators/envs/spot_env.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 41150e71a3..a1271e4bee 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -12,7 +12,6 @@ import matplotlib import numpy as np import pbrspot -import PIL.Image from bosdyn.client import RetryableRpcError, create_standard_sdk, math_helpers from bosdyn.client.lease import LeaseClient, LeaseKeepAlive from bosdyn.client.sdk import Robot @@ -22,7 +21,6 @@ from predicators import utils from predicators.envs import BaseEnv -from predicators.pretrained_model_interface import OpenAIVLM from predicators.settings import CFG from predicators.spot_utils.perception.object_detection import \ AprilTagObjectDetectionID, KnownStaticObjectDetectionID, \ @@ -102,8 +100,6 @@ class _PartialPerceptionState(State): in the classifier definitions for the dummy predicates """ - # obs_images: Optional[Dict[str, RGBDImageWithContext]] = None - @property def _simulator_state_predicates(self) -> Set[Predicate]: assert isinstance(self.simulator_state, Dict) @@ -865,9 +861,11 @@ def _actively_construct_env_task(self) -> EnvironmentTask: assert self._robot is not None assert self._localizer is not None - # TODO add logic for VLM predicates evaluation + # Prepare and evaluate VLM predicates for the initial state objects_in_view, vlm_atom_dict = self._actively_construct_initial_object_views( ) + vlm_predicates = _VLM_CLASSIFIER_PREDICATES + rgbd_images = capture_images(self._robot, self._localizer) gripper_open_percentage = get_robot_gripper_open_percentage( self._robot) @@ -879,10 +877,6 @@ def _actively_construct_env_task(self) -> EnvironmentTask: nonpercept_preds = self.predicates - self.percept_predicates assert all(a.predicate in nonpercept_preds for a in nonpercept_atoms) - # TODO Prepare and query VLM for VLM predicates - # vlm_atoms = None - vlm_predicates = _VLM_CLASSIFIER_PREDICATES - obs = _SpotObservation(rgbd_images, objects_in_view, set(), set(), self._spot_object, gripper_open_percentage, robot_pos, nonpercept_atoms, nonpercept_preds, From 93fb574cd89a7f19961e58c5d4df61f7ccb156da Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 18:22:47 -0400 Subject: [PATCH 42/71] fix no VLM case --- predicators/perception/spot_perceiver.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/predicators/perception/spot_perceiver.py b/predicators/perception/spot_perceiver.py index e28b8d9970..2a4f824a6a 100644 --- a/predicators/perception/spot_perceiver.py +++ b/predicators/perception/spot_perceiver.py @@ -207,6 +207,10 @@ def _update_state_from_observation(self, observation: Observation) -> None: self._camera_images = observation.images self._vlm_atom_dict = observation.vlm_atom_dict self._vlm_predicates = observation.vlm_predicates + else: + self._camera_images = None + self._vlm_atom_dict = None + self._vlm_predicates = None def _create_state(self) -> State: if self._waiting_for_observation: From cb0e1ee4faeddf1ed765cc325c318b3f47708a9c Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 18:28:41 -0400 Subject: [PATCH 43/71] add predicate prompt; fix and clean --- predicators/envs/spot_env.py | 60 ++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index a1271e4bee..5c38c9de28 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -754,7 +754,10 @@ def _build_realworld_observation( vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) vlm_atom_dict: Dict[VLMGroundAtom, bool or None] = vlm_predicate_batch_classify( - vlm_atoms, rgbds, True) + vlm_atoms, + rgbds, + predicates=vlm_predicates, + get_dict=True) # Update value if atoms in previous obs is None while new is not None for atom, result in vlm_atom_dict.items(): if curr_obs.vlm_atom_dict[atom] is None and result is not None: @@ -1045,12 +1048,13 @@ def _run_init_search_for_objects( rot=math_helpers.Quat.from_pitch( np.pi / 3)) move_hand_to_relative_pose(self._robot, hand_pose) + # Input VLM predicates (to filter task-relevant ones) and objects + # Obtain detections and additionally VLM ground atoms detections, artifacts, vlm_atom_dict = init_search_for_objects( self._robot, self._localizer, detection_ids, allowed_regions=self._allowed_regions, - # TODO input VLM predicates; to filter task-relevant ones vlm_predicates=_VLM_CLASSIFIER_PREDICATES, id2object=self._detection_id_to_obj, ) @@ -1077,12 +1081,6 @@ def _generate_goal_description(self) -> GoalDescription: """For now, we assume that there's only one goal per environment.""" -############################################################################### -# VLM Predicate Evaluation Related # -############################################################################### - -# TODO: move to a separate file - ############################################################################### # Shared Types, Predicates, Operators # ############################################################################### @@ -1507,13 +1505,13 @@ def _get_sweeping_surface_for_container(container: Object, # _on_classifier) _TopAbove = Predicate("TopAbove", [_base_object_type, _base_object_type], _top_above_classifier) -_Inside = Predicate("Inside", [_movable_object_type, _container_type], - _inside_classifier) +# _Inside = Predicate("Inside", [_movable_object_type, _container_type], +# _inside_classifier) _FitsInXY = Predicate("FitsInXY", [_movable_object_type, _base_object_type], _fits_in_xy_classifier) # NOTE: use this predicate instead if you want to disable inside checking. -_FakeInside = Predicate(_Inside.name, _Inside.types, - _create_dummy_predicate_classifier(_Inside)) +# _FakeInside = Predicate(_Inside.name, _Inside.types, +# _create_dummy_predicate_classifier(_Inside)) _NotInsideAnyContainer = Predicate("NotInsideAnyContainer", [_movable_object_type], _not_inside_any_container_classifier) @@ -1528,10 +1526,10 @@ def _get_sweeping_surface_for_container(container: Object, in_general_view_classifier) _Reachable = Predicate("Reachable", [_robot_type, _base_object_type], _reachable_classifier) -_Blocking = Predicate("Blocking", [_base_object_type, _base_object_type], - _blocking_classifier) -_NotBlocked = Predicate("NotBlocked", [_base_object_type], - _not_blocked_classifier) +# _Blocking = Predicate("Blocking", [_base_object_type, _base_object_type], +# _blocking_classifier) +# _NotBlocked = Predicate("NotBlocked", [_base_object_type], +# _not_blocked_classifier) _ContainerReadyForSweeping = Predicate( "ContainerReadyForSweeping", [_container_type, _immovable_object_type], _container_ready_for_sweeping_classifier) @@ -1550,14 +1548,30 @@ def _get_sweeping_surface_for_container(container: Object, "IsSemanticallyGreaterThan", [_base_object_type, _base_object_type], _is_semantically_greater_than_classifier) +# DEBUG hardcode now; CFG not updated here?! # if CFG.spot_vlm_eval_predicate: -# _On = VLMPredicate("On", [_movable_object_type, _base_object_type], -# _on_classifier) -# # _Inside = VLMPredicate("Inside", [_movable_object_type, _container_type], -# # _inside_classifier) - -_On = VLMPredicate("On", [_movable_object_type, _base_object_type], - _on_classifier) +tmp_vlm_flag = True + +if tmp_vlm_flag: + _On = VLMPredicate("On", [_movable_object_type, _base_object_type], None) + _Inside = VLMPredicate("Inside", [_movable_object_type, _container_type], + None) + _FakeInside = VLMPredicate(_Inside.name, _Inside.types, None) + _Blocking = VLMPredicate("Blocking", + [_base_object_type, _base_object_type], None) + _NotBlocked = VLMPredicate("NotBlocked", [_base_object_type], None) +else: + _On = Predicate("On", [_movable_object_type, _base_object_type], + _on_classifier) + _Inside = Predicate("Inside", [_movable_object_type, _container_type], + _inside_classifier) + # NOTE: use this predicate instead if you want to disable inside checking. + _FakeInside = Predicate(_Inside.name, _Inside.types, + _create_dummy_predicate_classifier(_Inside)) + _Blocking = Predicate("Blocking", [_base_object_type, _base_object_type], + _blocking_classifier) + _NotBlocked = Predicate("NotBlocked", [_base_object_type], + _not_blocked_classifier) # NOTE: Define all regular or VLM predicates above _ALL_PREDICATES = { From 63a14293c8870f8db96d026b7d8d8c27ca4591aa Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 18:33:18 -0400 Subject: [PATCH 44/71] add predicate prompt & some logging; fix and clean --- predicators/settings.py | 1 + .../perception/object_perception.py | 38 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/predicators/settings.py b/predicators/settings.py index ed04ffc7cf..56dae5504d 100644 --- a/predicators/settings.py +++ b/predicators/settings.py @@ -183,6 +183,7 @@ class GlobalSettings: spot_sweep_env_goal_description = "get the objects into the bucket" # Evaluate some predicates with VLM; need additional setup; WIP spot_vlm_eval_predicate = False + vlm_eval_verbose = False # pddl blocks env parameters pddl_blocks_procedural_train_min_num_blocks = 3 diff --git a/predicators/spot_utils/perception/object_perception.py b/predicators/spot_utils/perception/object_perception.py index f95194caa9..4fc9b89340 100644 --- a/predicators/spot_utils/perception/object_perception.py +++ b/predicators/spot_utils/perception/object_perception.py @@ -2,11 +2,12 @@ import logging import time -from typing import Dict, List, Sequence, Set +from typing import Dict, List, Optional, Sequence, Set import PIL.Image from predicators.pretrained_model_interface import OpenAIVLM +from predicators.settings import CFG from predicators.spot_utils.perception.perception_structs import \ RGBDImageWithContext from predicators.structs import Object, State, VLMGroundAtom, VLMPredicate @@ -47,6 +48,11 @@ Inside(object1, container) Blocking(object1, object2) On(object, surface) + +Here are VLM predicates we have, note that they are defined over typed variables. +Example: ( : ...) +VLM Predicates (separated by line or newline character): +{vlm_predicates} Examples (separated by line or newline character): Do these predicates hold in the following images? @@ -55,6 +61,7 @@ Blocking(apple:object, orange:object) Blocking(apple:object, apple:object) On(apple:object, apple:object) +On(apple:object, bowl:container) Answer (in a single word Yes/No/Unknown for each question, unknown if can't tell from given images): Yes @@ -62,6 +69,7 @@ Unknown No No +No Actual questions (separated by line or newline character): Do these predicates hold in the following images? @@ -95,7 +103,7 @@ def vlm_predicate_classify(question: str, state: State) -> bool | None: seed=int(time.time()), num_completions=1, ) - logging.info(f"VLM response 0: {vlm_responses[0]}") + logging.debug(f"VLM response 0: {vlm_responses[0]}") vlm_response = vlm_responses[0].strip().lower() if vlm_response == "yes": @@ -111,8 +119,10 @@ def vlm_predicate_classify(question: str, state: State) -> bool | None: def vlm_predicate_batch_query( - queries: List[str], images: Dict[str, - RGBDImageWithContext]) -> List[bool]: + queries: List[str], + images: Dict[str, RGBDImageWithContext], + predicate_prompts: Optional[List[str]] = None, +) -> List[bool]: """Use queries generated from VLM predicates to evaluate them via VLM in batch. @@ -122,14 +132,19 @@ def vlm_predicate_batch_query( # Assemble the full prompt question = '\n'.join(queries) - full_prompt = vlm_predicate_batch_eval_prompt.format(question=question) + vlm_predicates = '\n'.join(predicate_prompts) if predicate_prompts else '' + full_prompt = vlm_predicate_batch_eval_prompt.format( + vlm_predicates=vlm_predicates, question=question) image_list = [ PIL.Image.fromarray(v.rotated_rgb) for _, v in images.items() ] - logging.info(f"VLM predicate evaluation for: {question}") - logging.info(f"Prompt: {full_prompt}") + logging.info(f"VLM predicate evaluation for: \n{question}") + if CFG.vlm_eval_verbose: + logging.info(f"Prompt: {full_prompt}") + else: + logging.debug(f"Prompt: {full_prompt}") vlm_responses = vlm.sample_completions( prompt=full_prompt, @@ -138,7 +153,7 @@ def vlm_predicate_batch_query( seed=int(time.time()), num_completions=1, ) - logging.info(f"VLM response 0: {vlm_responses[0]}") + logging.debug(f"VLM response 0: {vlm_responses[0]}") # Parse the responses responses = vlm_responses[0].strip().lower().split('\n') @@ -161,11 +176,16 @@ def vlm_predicate_batch_query( def vlm_predicate_batch_classify( atoms: Set[VLMGroundAtom], images: Dict[str, RGBDImageWithContext], + predicates: Optional[Set[VLMPredicate]] = None, get_dict: bool = True ) -> Dict[VLMGroundAtom, bool] | Set[VLMGroundAtom]: """Use VLM to evaluate a set of atoms in a given state.""" # Get the queries for the atoms queries = [atom.get_query_str() for atom in atoms] + if predicates is not None: + predicate_prompts = [p.pddl_str() for p in predicates] + else: + predicate_prompts = None if len(queries) == 0: return {} @@ -173,7 +193,7 @@ def vlm_predicate_batch_classify( logging.info(f"VLM predicate evaluation queries: {queries}") # Call VLM to evaluate the queries - results = vlm_predicate_batch_query(queries, images) + results = vlm_predicate_batch_query(queries, images, predicate_prompts) # Update the atoms with the results if get_dict: From ba2575fef3e8ebb22731b5a3d9723eed0e12b87b Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 18:34:52 -0400 Subject: [PATCH 45/71] overwrite vlm predicate classifier; reformat --- predicators/structs.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/predicators/structs.py b/predicators/structs.py index 0141938c36..a24d8f0b08 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -184,7 +184,7 @@ def allclose(self, other: State) -> bool: """Return whether this state is close enough to another one, i.e., its objects are the same, and the features are close.""" if self.simulator_state is not None or \ - other.simulator_state is not None: + other.simulator_state is not None: raise NotImplementedError("Cannot use allclose when " "simulator_state is not None.") @@ -206,7 +206,7 @@ def pretty_str(self) -> str: if obj.type not in type_to_table: type_to_table[obj.type] = [] type_to_table[obj.type].append([obj.name] + \ - list(map(str, self[obj]))) + list(map(str, self[obj]))) table_strs = [] for t in sorted(type_to_table): headers = ["type: " + t.name] + list(t.feature_names) @@ -325,6 +325,8 @@ class VLMPredicate(Predicate): at once. """ + _classifier: Optional[Callable[[State, Sequence[Object]], bool]] = None + def holds(self, state: State, objects: Sequence[Object]) -> bool: """Public method for getting predicate value. @@ -732,8 +734,8 @@ def pddl_str(self) -> str: for i, t in enumerate(pred.types)) pred_eff_variables_str = " ".join(f"?x{i}" for i in range(pred.arity)) - effects_str += f"(forall ({pred_types_str})" +\ - f" (not ({pred.name} {pred_eff_variables_str})))" + effects_str += f"(forall ({pred_types_str})" + \ + f" (not ({pred.name} {pred_eff_variables_str})))" effects_str += "\n " return f"""(:action {self.name} :parameters ({params_str}) @@ -1586,7 +1588,7 @@ def __post_init__(self) -> None: # The preconditions and goal preconditions should only use variables in # the rule parameters. for atom in self.pos_state_preconditions | \ - self.neg_state_preconditions | self.goal_preconditions: + self.neg_state_preconditions | self.goal_preconditions: assert all(v in self.parameters for v in atom.variables) @lru_cache(maxsize=None) From a11e5a18417b5eeb35e484b0adf5859b3922eac6 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 18:35:14 -0400 Subject: [PATCH 46/71] update --- predicators/pretrained_model_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/predicators/pretrained_model_interface.py b/predicators/pretrained_model_interface.py index 7b8834c4c0..b33c349d89 100644 --- a/predicators/pretrained_model_interface.py +++ b/predicators/pretrained_model_interface.py @@ -10,7 +10,7 @@ import os import time from io import BytesIO -from typing import List, Optional, Dict +from typing import Dict, List, Optional import cv2 import google From 8ba4c5dd5a640d0834b1c5ca0b6604c8f6271634 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 18:37:21 -0400 Subject: [PATCH 47/71] update vlm query in obj finding --- .../spot_utils/skills/spot_find_objects.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index 1d710d85bd..36349843ff 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -1,18 +1,18 @@ """Interface for finding objects by moving around and running detection.""" -import logging import time from collections import defaultdict -from typing import Any, Callable, Collection, Dict, List, Optional, Sequence, \ - Set, Tuple +from typing import Any, Collection, Dict, List, Optional, Sequence, Set, Tuple import numpy as np from bosdyn.client import math_helpers from bosdyn.client.lease import LeaseClient from bosdyn.client.math_helpers import SE3Pose from bosdyn.client.sdk import Robot +from rich import print from scipy.spatial import Delaunay from predicators import utils +from predicators.settings import CFG from predicators.spot_utils.perception.object_detection import detect_objects from predicators.spot_utils.perception.object_perception import \ get_vlm_atom_combinations, vlm_predicate_batch_classify @@ -81,16 +81,22 @@ def _find_objects_with_choreographed_moves( print(f"Remaining objects: {remaining_object_ids}") # Get VLM queries + Send request - if len(all_detections) > 0 and len(vlm_predicates) > 0: + # TODO: We may query objects only in current view's images. + # Now we query all detected objects in all past views. + if CFG.spot_vlm_eval_predicate and len(all_detections) > 0 and len( + vlm_predicates) > 0: objects = [id2object[id_] for id_ in all_detections] vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) vlm_atom_dict = vlm_predicate_batch_classify( - vlm_atoms, rgbds, True) + vlm_atoms, rgbds, predicates=vlm_predicates, get_dict=True) # Update value if original is None while new is not None for atom, result in vlm_atom_dict.items(): if all_vlm_atom_dict[atom] is None and result is not None: all_vlm_atom_dict[atom] = result - print(f"Calculated VLM atoms: {all_vlm_atom_dict}") + print(f"Calculated VLM atoms: {dict(vlm_atom_dict)}") + print( + f"True VLM atoms (with values as True): {dict(filter(lambda it: it[1], all_vlm_atom_dict.items()))}" + ) else: # No VLM predicates or no objects found yet pass From 372c1915892e7e6d30e1f8dedb72aedfe5a63477 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 18:48:48 -0400 Subject: [PATCH 48/71] add a simple pick place task - pick block and place into bowl --- predicators/envs/spot_env.py | 53 ++++++++++++++++++++++++ predicators/perception/spot_perceiver.py | 7 ++++ 2 files changed, 60 insertions(+) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 5c38c9de28..0978657ffb 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -3217,3 +3217,56 @@ def _generate_goal_description(self) -> GoalDescription: def _get_dry_task(self, train_or_test: str, task_idx: int) -> EnvironmentTask: raise NotImplementedError("Dry task generation not implemented.") + + +class LISSpotBlockBowlEnv(SpotRearrangementEnv): + """An extremely basic environment where a block needs to be placed in a + bowl and is specifically used for testing in the LIS Spot room. + + Very simple and mostly just for testing. + """ + + def __init__(self, use_gui: bool = True) -> None: + super().__init__(use_gui) + + op_to_name = {o.name: o for o in _create_operators()} + op_names_to_keep = { + "MoveToReachObject", + "MoveToHandViewObject", + "PickObjectFromTop", + "PlaceObjectOnTop", + "DropObjectInside", + } + self._strips_operators = {op_to_name[o] for o in op_names_to_keep} + + @classmethod + def get_name(cls) -> str: + return "lis_spot_block_bowl_env" + + @property + def _detection_id_to_obj(self) -> Dict[ObjectDetectionID, Object]: + + detection_id_to_obj: Dict[ObjectDetectionID, Object] = {} + + red_block = Object("red_block", _movable_object_type) + red_block_detection = LanguageObjectDetectionID( + "red block/orange block/yellow block") + detection_id_to_obj[red_block_detection] = red_block + + green_bowl = Object("green_bowl", _container_type) + green_bowl_detection = LanguageObjectDetectionID( + "green bowl/greenish bowl") + detection_id_to_obj[green_bowl_detection] = green_bowl + + for obj, pose in get_known_immovable_objects().items(): + detection_id = KnownStaticObjectDetectionID(obj.name, pose) + detection_id_to_obj[detection_id] = obj + + return detection_id_to_obj + + def _generate_goal_description(self) -> GoalDescription: + return "pick the red block into the green bowl" + + def _get_dry_task(self, train_or_test: str, + task_idx: int) -> EnvironmentTask: + raise NotImplementedError("Dry task generation not implemented.") diff --git a/predicators/perception/spot_perceiver.py b/predicators/perception/spot_perceiver.py index 2a4f824a6a..c47b173501 100644 --- a/predicators/perception/spot_perceiver.py +++ b/predicators/perception/spot_perceiver.py @@ -486,6 +486,13 @@ def _create_goal(self, state: State, block = Object("red_block", _movable_object_type) Holding = pred_name_to_pred["Holding"] return {GroundAtom(Holding, [robot, block])} + if goal_description == "pick the red block into the green bowl": + block = Object("red_block", _movable_object_type) + bowl = Object("green_bowl", _container_type) + Inside = pred_name_to_pred["Inside"] + return { + GroundAtom(Inside, [block, bowl]), + } if goal_description == "setup sweeping": robot = Object("robot", _robot_type) brush = Object("brush", _movable_object_type) From 3019b9764daed2f909fc2a3226dfd6f5fe17143f Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 18:48:55 -0400 Subject: [PATCH 49/71] update --- predicators/ground_truth_models/spot_env/nsrts.py | 14 ++++++++++---- .../ground_truth_models/spot_env/options.py | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/predicators/ground_truth_models/spot_env/nsrts.py b/predicators/ground_truth_models/spot_env/nsrts.py index 8ab36470a6..6ca7ce3ab8 100644 --- a/predicators/ground_truth_models/spot_env/nsrts.py +++ b/predicators/ground_truth_models/spot_env/nsrts.py @@ -285,10 +285,16 @@ class SpotEnvsGroundTruthNSRTFactory(GroundTruthNSRTFactory): @classmethod def get_env_names(cls) -> Set[str]: return { - "spot_cube_env", "spot_soda_floor_env", "spot_soda_table_env", - "spot_soda_bucket_env", "spot_soda_chair_env", - "spot_main_sweep_env", "spot_ball_and_cup_sticky_table_env", - "spot_brush_shelf_env", "lis_spot_block_floor_env" + "spot_cube_env", + "spot_soda_floor_env", + "spot_soda_table_env", + "spot_soda_bucket_env", + "spot_soda_chair_env", + "spot_main_sweep_env", + "spot_ball_and_cup_sticky_table_env", + "spot_brush_shelf_env", + "lis_spot_block_floor_env", + "lis_spot_block_bowl_env", } @staticmethod diff --git a/predicators/ground_truth_models/spot_env/options.py b/predicators/ground_truth_models/spot_env/options.py index 4729a50493..b147500507 100644 --- a/predicators/ground_truth_models/spot_env/options.py +++ b/predicators/ground_truth_models/spot_env/options.py @@ -996,6 +996,7 @@ def get_env_names(cls) -> Set[str]: "spot_ball_and_cup_sticky_table_env", "spot_brush_shelf_env", "lis_spot_block_floor_env", + "lis_spot_block_bowl_env", } @classmethod From f3e6a454fa46e7e114c6d463c0459c18fbcd67c3 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 18:49:04 -0400 Subject: [PATCH 50/71] update obj metadata --- .../graph_nav_maps/b45-621/metadata.yaml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml b/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml index cb37e2d951..efbf486d6d 100644 --- a/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml +++ b/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml @@ -46,4 +46,17 @@ static-object-features: length: 0.1 width: 0.1 placeable: 1 - is_sweeper: 0 \ No newline at end of file + is_sweeper: 0 + green_bowl: + shape: 2 + height: 0.2 + length: 0.2 + width: 0.2 + placeable: 0 + is_sweeper: 0 + +# NOTE: Not sure what these mean, but have to be there? +prepare_container_relative_xy: + dx: -0.1 + dy: 0.1 + angle: -1.5707 # - pi / 2 \ No newline at end of file From 7c82fde270517fa3c7da29a0101f64d5d89a861c Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 20:59:38 -0400 Subject: [PATCH 51/71] minor --- predicators/spot_utils/skills/spot_find_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index 1d710d85bd..824a6a4eb8 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -85,7 +85,7 @@ def _find_objects_with_choreographed_moves( objects = [id2object[id_] for id_ in all_detections] vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) vlm_atom_dict = vlm_predicate_batch_classify( - vlm_atoms, rgbds, True) + vlm_atoms, rgbds, vlm_predicates, True) # Update value if original is None while new is not None for atom, result in vlm_atom_dict.items(): if all_vlm_atom_dict[atom] is None and result is not None: From 499f6394db33698645586f2cc1a4514333b66e2c Mon Sep 17 00:00:00 2001 From: Linfeng Date: Thu, 9 May 2024 21:00:07 -0400 Subject: [PATCH 52/71] ?? change width/radius again, now okay --- .../spot_utils/graph_nav_maps/b45-621/metadata.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml b/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml index efbf486d6d..efcaecf9c2 100644 --- a/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml +++ b/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml @@ -47,13 +47,15 @@ static-object-features: width: 0.1 placeable: 1 is_sweeper: 0 + radius: 0.1 # TODO quick fix green_bowl: shape: 2 - height: 0.2 - length: 0.2 - width: 0.2 + height: 0.5 + length: 0.5 + width: 0.5 placeable: 0 is_sweeper: 0 + radius: 0.2 # TODO quick fox # NOTE: Not sure what these mean, but have to be there? prepare_container_relative_xy: From 51bf5691875af7a5ee615bb8b0a0589101482587 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 16:55:25 -0400 Subject: [PATCH 53/71] add prompt to vlm predicate --- predicators/structs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/predicators/structs.py b/predicators/structs.py index a24d8f0b08..8f113ce2af 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -463,6 +463,7 @@ class VLMGroundAtom(GroundAtom): # NOTE: This subclasses GroundAtom to support VLM predicates and classifiers predicate: VLMPredicate + prompt: Optional[str] = None def get_query_str(self, without_type: bool = False) -> str: """Get a query string for this ground atom. @@ -476,6 +477,9 @@ def get_query_str(self, without_type: bool = False) -> str: o.name for o in self.objects) + ")" else: string = str(self) + + if self.prompt is not None: + string += f" [Prompt: {self.prompt}]" return string def holds(self, state: State) -> bool: From 86ef996b63d32a40dc1aad80d3a493fdadfbc563 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 16:55:46 -0400 Subject: [PATCH 54/71] minor --- predicators/spot_utils/perception/object_perception.py | 8 +++----- predicators/spot_utils/skills/spot_find_objects.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/predicators/spot_utils/perception/object_perception.py b/predicators/spot_utils/perception/object_perception.py index 4fc9b89340..4898f4bc84 100644 --- a/predicators/spot_utils/perception/object_perception.py +++ b/predicators/spot_utils/perception/object_perception.py @@ -197,13 +197,11 @@ def vlm_predicate_batch_classify( # Update the atoms with the results if get_dict: + # Return all ground atoms with True/False/None return {atom: result for atom, result in zip(atoms, results)} else: - hold_atoms = set() - for atom, result in zip(atoms, results): - if result: - hold_atoms.add(atom) - return hold_atoms + # Only return True ground atoms + return {atom for atom, result in zip(atoms, results) if result} def get_vlm_atom_combinations(objects: Sequence[Object], diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index 824a6a4eb8..fb7083cffb 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -90,7 +90,7 @@ def _find_objects_with_choreographed_moves( for atom, result in vlm_atom_dict.items(): if all_vlm_atom_dict[atom] is None and result is not None: all_vlm_atom_dict[atom] = result - print(f"Calculated VLM atoms: {all_vlm_atom_dict}") + print(f"Calculated VLM atoms: {dict(all_vlm_atom_dict)}") else: # No VLM predicates or no objects found yet pass From 6db3fbe840a977680a3c0539908346c28f629e2a Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 18:17:23 -0400 Subject: [PATCH 55/71] minor --- predicators/envs/spot_env.py | 2 ++ predicators/spot_utils/skills/spot_find_objects.py | 1 + 2 files changed, 3 insertions(+) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 0978657ffb..919ea590f0 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -1260,6 +1260,8 @@ def _fits_in_xy_classifier(state: State, objects: Sequence[Object]) -> bool: for obj in objects: obj_geom = object_to_top_down_geom(obj, state) if isinstance(obj_geom, utils.Rectangle): + # DEBUG check default value - why radius not None by default? + # DEBUG fix the value if obj is contained: radius = max(obj_geom.width / 2, obj_geom.height / 2) else: diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index fb7083cffb..ce5c5ec2e2 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -11,6 +11,7 @@ from bosdyn.client.math_helpers import SE3Pose from bosdyn.client.sdk import Robot from scipy.spatial import Delaunay +from rich import print from predicators import utils from predicators.spot_utils.perception.object_detection import detect_objects From d6daa39dd3de60ad8db3ebdcc6f842406c45a6ef Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 18:47:50 -0400 Subject: [PATCH 56/71] update VLM atom update rule; add prompt to VLM predicate --- predicators/envs/spot_env.py | 43 +++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 919ea590f0..c0785df57f 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -752,15 +752,16 @@ def _build_realworld_observation( # Use currently visible objects to generate atom combinations objects = list(all_objects_in_view.keys()) vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) - vlm_atom_dict: Dict[VLMGroundAtom, - bool or None] = vlm_predicate_batch_classify( - vlm_atoms, - rgbds, - predicates=vlm_predicates, - get_dict=True) - # Update value if atoms in previous obs is None while new is not None + vlm_atom_dict: Dict[VLMGroundAtom, bool or None] = vlm_predicate_batch_classify( + vlm_atoms, + rgbds, + predicates=vlm_predicates, + get_dict=True + ) + # Update VLM atom value if the ground atom value is not None for atom, result in vlm_atom_dict.items(): - if curr_obs.vlm_atom_dict[atom] is None and result is not None: + # TODO make sure we can add new vlm atom! + if result is not None: vlm_atom_dict[atom] = result else: @@ -1555,13 +1556,25 @@ def _get_sweeping_surface_for_container(container: Object, tmp_vlm_flag = True if tmp_vlm_flag: - _On = VLMPredicate("On", [_movable_object_type, _base_object_type], None) - _Inside = VLMPredicate("Inside", [_movable_object_type, _container_type], - None) - _FakeInside = VLMPredicate(_Inside.name, _Inside.types, None) - _Blocking = VLMPredicate("Blocking", - [_base_object_type, _base_object_type], None) - _NotBlocked = VLMPredicate("NotBlocked", [_base_object_type], None) + _On = VLMPredicate( + "On", [_movable_object_type, _base_object_type], + prompt="This predicate typically describes a movable object on a flat surface, so it's in conflict with the object being inside a container. Please check the image and confirm the object is on the surface." + ) + _Inside = VLMPredicate( + "Inside", [_movable_object_type, _container_type], + prompt="This typically describes an object inside a container, so it's in conflict with the object being on a surface. Please check the image and confirm the object is inside the container." + ) + _FakeInside = VLMPredicate( + _Inside.name, _Inside.types, + ) + # NOTE: Check the classifier; try to make it consistent or document how this VLM predicate is different/better. + _Blocking = VLMPredicate( + "Blocking", + [_base_object_type, _base_object_type], + prompt="This means if an object is blocking the Spot robot approaching another one.") + _NotBlocked = VLMPredicate( + "NotBlocked", [_base_object_type], + prompt="The given object is not blocked by any other object.") else: _On = Predicate("On", [_movable_object_type, _base_object_type], _on_classifier) From 7a4d20e8fe57d664f1042e88f72932930c20c98b Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 18:48:03 -0400 Subject: [PATCH 57/71] add prompt field to VLM predicate / atom --- predicators/structs.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/predicators/structs.py b/predicators/structs.py index 8f113ce2af..f5363e9f3b 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -315,6 +315,7 @@ def _negated_classifier(self, state: State, return not self._classifier(state, objects) +@dataclass(frozen=True, order=True, repr=False) class VLMPredicate(Predicate): """Struct defining a predicate (a lifted classifier over states) that uses a VLM for evaluation. @@ -325,7 +326,10 @@ class VLMPredicate(Predicate): at once. """ + # A classifier is not needed for VLM predicates _classifier: Optional[Callable[[State, Sequence[Object]], bool]] = None + # An optional prompt additionally provided for each VLM predicate + prompt: Optional[str] = None def holds(self, state: State, objects: Sequence[Object]) -> bool: """Public method for getting predicate value. @@ -463,7 +467,6 @@ class VLMGroundAtom(GroundAtom): # NOTE: This subclasses GroundAtom to support VLM predicates and classifiers predicate: VLMPredicate - prompt: Optional[str] = None def get_query_str(self, without_type: bool = False) -> str: """Get a query string for this ground atom. @@ -478,8 +481,8 @@ def get_query_str(self, without_type: bool = False) -> str: else: string = str(self) - if self.prompt is not None: - string += f" [Prompt: {self.prompt}]" + if self.predicate.prompt is not None: + string += f" [Prompt: {self.predicate.prompt}]" return string def holds(self, state: State) -> bool: From ff770cec5fc9776f855650d154760dde5bedd624 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 18:59:58 -0400 Subject: [PATCH 58/71] reformat --- predicators/envs/spot_env.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index c0785df57f..ea40376bb2 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -752,12 +752,12 @@ def _build_realworld_observation( # Use currently visible objects to generate atom combinations objects = list(all_objects_in_view.keys()) vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) - vlm_atom_dict: Dict[VLMGroundAtom, bool or None] = vlm_predicate_batch_classify( - vlm_atoms, - rgbds, - predicates=vlm_predicates, - get_dict=True - ) + vlm_atom_dict: Dict[VLMGroundAtom, + bool or None] = vlm_predicate_batch_classify( + vlm_atoms, + rgbds, + predicates=vlm_predicates, + get_dict=True) # Update VLM atom value if the ground atom value is not None for atom, result in vlm_atom_dict.items(): # TODO make sure we can add new vlm atom! @@ -1556,22 +1556,28 @@ def _get_sweeping_surface_for_container(container: Object, tmp_vlm_flag = True if tmp_vlm_flag: + # _On = Predicate("On", [_movable_object_type, _base_object_type], + # _on_classifier) _On = VLMPredicate( "On", [_movable_object_type, _base_object_type], - prompt="This predicate typically describes a movable object on a flat surface, so it's in conflict with the object being inside a container. Please check the image and confirm the object is on the surface." + prompt= + "This predicate typically describes a movable object on a flat surface, so it's in conflict with the object being inside a container. Please check the image and confirm the object is on the surface." ) _Inside = VLMPredicate( "Inside", [_movable_object_type, _container_type], - prompt="This typically describes an object inside a container, so it's in conflict with the object being on a surface. Please check the image and confirm the object is inside the container." + prompt= + "This typically describes an object inside a container, so it's in conflict with the object being on a surface. Please check the image and confirm the object is inside the container." ) _FakeInside = VLMPredicate( - _Inside.name, _Inside.types, + _Inside.name, + _Inside.types, ) # NOTE: Check the classifier; try to make it consistent or document how this VLM predicate is different/better. _Blocking = VLMPredicate( - "Blocking", - [_base_object_type, _base_object_type], - prompt="This means if an object is blocking the Spot robot approaching another one.") + "Blocking", [_base_object_type, _base_object_type], + prompt= + "This means if an object is blocking the Spot robot approaching another one." + ) _NotBlocked = VLMPredicate( "NotBlocked", [_base_object_type], prompt="The given object is not blocked by any other object.") From 0175868ae75e5ed99e74ddd519d18788f2fdef8b Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 19:00:27 -0400 Subject: [PATCH 59/71] fix hash error - override hash func again --- predicators/structs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/predicators/structs.py b/predicators/structs.py index f5363e9f3b..aa011b6f0f 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -331,6 +331,10 @@ class VLMPredicate(Predicate): # An optional prompt additionally provided for each VLM predicate prompt: Optional[str] = None + def __hash__(self) -> int: + """Have to add this to override the default hash method again.""" + return self._hash + def holds(self, state: State, objects: Sequence[Object]) -> bool: """Public method for getting predicate value. From 083ac9ffbd8be58ddaa728296679dd87c98c7334 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 19:37:53 -0400 Subject: [PATCH 60/71] update atom print logic --- predicators/spot_utils/skills/spot_find_objects.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index 36349843ff..af7f51c459 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -93,10 +93,6 @@ def _find_objects_with_choreographed_moves( for atom, result in vlm_atom_dict.items(): if all_vlm_atom_dict[atom] is None and result is not None: all_vlm_atom_dict[atom] = result - print(f"Calculated VLM atoms: {dict(vlm_atom_dict)}") - print( - f"True VLM atoms (with values as True): {dict(filter(lambda it: it[1], all_vlm_atom_dict.items()))}" - ) else: # No VLM predicates or no objects found yet pass @@ -121,6 +117,13 @@ def _find_objects_with_choreographed_moves( all_detections.update(detections) all_artifacts.update(artifacts) + # Logging + print(f"Calculated VLM atoms (in all views): {dict(all_vlm_atom_dict)}") + print( + f"True VLM atoms (in all views; with values as True): " + f"{dict(filter(lambda it: it[1], all_vlm_atom_dict.items()))}" + ) + # Close the gripper. if open_and_close_gripper: close_gripper(robot) From 9bd79495b587fe111d9a991a504eee9bd6595e8f Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 19:38:11 -0400 Subject: [PATCH 61/71] update print for predicate eval --- predicators/spot_utils/perception/object_perception.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/predicators/spot_utils/perception/object_perception.py b/predicators/spot_utils/perception/object_perception.py index 4898f4bc84..0ee8a5fb8b 100644 --- a/predicators/spot_utils/perception/object_perception.py +++ b/predicators/spot_utils/perception/object_perception.py @@ -93,7 +93,7 @@ def vlm_predicate_classify(question: str, state: State) -> bool | None: PIL.Image.fromarray(v.rotated_rgb) for _, v in images_dict.items() ] - logging.info(f"VLM predicate evaluation for: {question}") + logging.info(f"VLM predicate evaluation for: \n{question}") logging.info(f"Prompt: {full_prompt}") vlm_responses = vlm.sample_completions( @@ -140,7 +140,7 @@ def vlm_predicate_batch_query( PIL.Image.fromarray(v.rotated_rgb) for _, v in images.items() ] - logging.info(f"VLM predicate evaluation for: \n{question}") + logging.info(f"VLM predicate evaluation input (with prompt): \n{question}") if CFG.vlm_eval_verbose: logging.info(f"Prompt: {full_prompt}") else: @@ -190,7 +190,8 @@ def vlm_predicate_batch_classify( if len(queries) == 0: return {} - logging.info(f"VLM predicate evaluation queries: {queries}") + queries_print = [atom.get_query_str(include_prompt=False) for atom in atoms] + logging.info(f"VLM predicate evaluation queries: {queries_print}") # Call VLM to evaluate the queries results = vlm_predicate_batch_query(queries, images, predicate_prompts) From ad1f754d1120420ff1fb46054376c47c81064625 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 19:39:21 -0400 Subject: [PATCH 62/71] got fix: pick and place working! add curr obs atoms to last state, return all --- predicators/envs/spot_env.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index ea40376bb2..63d7da9ccb 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -1,5 +1,6 @@ """Basic environment for the Boston Dynamics Spot Robot.""" import abc +import copy import functools import json import logging @@ -752,28 +753,37 @@ def _build_realworld_observation( # Use currently visible objects to generate atom combinations objects = list(all_objects_in_view.keys()) vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) - vlm_atom_dict: Dict[VLMGroundAtom, + vlm_atom_new: Dict[VLMGroundAtom, bool or None] = vlm_predicate_batch_classify( vlm_atoms, rgbds, predicates=vlm_predicates, get_dict=True) - # Update VLM atom value if the ground atom value is not None - for atom, result in vlm_atom_dict.items(): - # TODO make sure we can add new vlm atom! + + # Update VLM atom value if the new ground atom value is not None + # Otherwise, use the value in current obs + vlm_atom_return = copy.deepcopy(curr_obs.vlm_atom_dict) + for atom, result in vlm_atom_new.items(): if result is not None: - vlm_atom_dict[atom] = result + vlm_atom_return[atom] = result + + # Logging + print(f"Calculated VLM atoms (in current obs): {dict(vlm_atom_new)}") + print( + f"True VLM atoms (after updated with current obs): " + f"{dict(filter(lambda it: it[1], vlm_atom_return.items()))}" + ) else: vlm_predicates = set() - vlm_atom_dict = {} + vlm_atom_return = {} obs = _SpotObservation(rgbds, all_objects_in_view, objects_in_hand_view, objects_in_any_view_except_back, self._spot_object, gripper_open_percentage, robot_pos, nonpercept_atoms, nonpercept_preds, - vlm_atom_dict, vlm_predicates) + vlm_atom_return, vlm_predicates) return obs From 0f0e8ef618fee6057c2ddad3a966e5658e2ecf48 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 19:39:35 -0400 Subject: [PATCH 63/71] minor --- predicators/structs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/predicators/structs.py b/predicators/structs.py index aa011b6f0f..688ab03f1a 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -472,7 +472,7 @@ class VLMGroundAtom(GroundAtom): # NOTE: This subclasses GroundAtom to support VLM predicates and classifiers predicate: VLMPredicate - def get_query_str(self, without_type: bool = False) -> str: + def get_query_str(self, without_type: bool = False, include_prompt: bool = True) -> str: """Get a query string for this ground atom. Instead of directly evaluating the ground atom, we will use the @@ -485,7 +485,7 @@ def get_query_str(self, without_type: bool = False) -> str: else: string = str(self) - if self.predicate.prompt is not None: + if self.predicate.prompt is not None and include_prompt: string += f" [Prompt: {self.predicate.prompt}]" return string From 5004878393916dae74fccf8bb6b192b2b106f1d5 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 19:39:55 -0400 Subject: [PATCH 64/71] try update container dx value --- predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml b/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml index efcaecf9c2..e2e942ddcb 100644 --- a/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml +++ b/predicators/spot_utils/graph_nav_maps/b45-621/metadata.yaml @@ -59,6 +59,6 @@ static-object-features: # NOTE: Not sure what these mean, but have to be there? prepare_container_relative_xy: - dx: -0.1 + dx: -1.0 dy: 0.1 angle: -1.5707 # - pi / 2 \ No newline at end of file From 8e0c4e02d10cec184c61a0963ccbcc81898d3eff Mon Sep 17 00:00:00 2001 From: Linfeng Date: Fri, 10 May 2024 19:43:52 -0400 Subject: [PATCH 65/71] minor --- predicators/envs/spot_env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 63d7da9ccb..834cb05f18 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -768,8 +768,8 @@ def _build_realworld_observation( vlm_atom_return[atom] = result # Logging - print(f"Calculated VLM atoms (in current obs): {dict(vlm_atom_new)}") - print( + logging.info(f"Calculated VLM atoms (in current obs): {dict(vlm_atom_new)}") + logging.info( f"True VLM atoms (after updated with current obs): " f"{dict(filter(lambda it: it[1], vlm_atom_return.items()))}" ) From 83484fbee5446567464544b5febf7a392ab56072 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Sat, 11 May 2024 13:36:33 -0400 Subject: [PATCH 66/71] rich table print util --- predicators/utils.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/predicators/utils.py b/predicators/utils.py index 7293848f78..9964c9e5b7 100644 --- a/predicators/utils.py +++ b/predicators/utils.py @@ -50,6 +50,9 @@ Heuristic as _PyperplanBaseHeuristic from pyperplan.planner import HEURISTICS as _PYPERPLAN_HEURISTICS from scipy.stats import beta as BetaRV +import rich.table +from rich.console import Console +from rich.text import Text from predicators.args import create_arg_parser from predicators.pybullet_helpers.joint import JointPositions @@ -3863,3 +3866,13 @@ def run_ground_nsrt_with_assertions(ground_nsrt: _GroundNSRT, assert not atom.holds(state), \ f"Delete effect for {ground_nsrt_str} failed: {atom}" return state + + +def log_rich_table(rich_table: rich.table.Table) -> "Texssas": + """Generate an ascii formatted presentation of a Rich table + Eliminates any column styling + """ + console = Console(width=150) + with console.capture() as capture: + console.print(rich_table) + return Text.from_ansi(capture.get()) From 5898a1f7695db1408f157ec624dc9fa796a53b1d Mon Sep 17 00:00:00 2001 From: Linfeng Date: Sat, 11 May 2024 13:37:44 -0400 Subject: [PATCH 67/71] add table; to fix for object find! --- .../spot_utils/skills/spot_find_objects.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index af7f51c459..ac0801228e 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -9,6 +9,7 @@ from bosdyn.client.math_helpers import SE3Pose from bosdyn.client.sdk import Robot from rich import print +from rich.table import Table from scipy.spatial import Delaunay from predicators import utils @@ -124,6 +125,13 @@ def _find_objects_with_choreographed_moves( f"{dict(filter(lambda it: it[1], all_vlm_atom_dict.items()))}" ) + table = Table(title="Evaluated VLM atoms (in all views)") + table.add_column("Atom", style="cyan") + table.add_column("Value", style="magenta") + for atom, result in all_vlm_atom_dict.items(): + table.add_row(str(atom), str(result)) + print(table) + # Close the gripper. if open_and_close_gripper: close_gripper(robot) @@ -204,13 +212,18 @@ def step_back_to_find_objects( # Don't open and close the gripper because we need the object to be # in view when the action has finished, and we can't leave the gripper # open because then HandEmpty will misfire. - _find_objects_with_choreographed_moves(robot, - localizer, - object_ids, - base_moves, - hand_moves, - open_and_close_gripper=False, - allowed_regions=allowed_regions) + _find_objects_with_choreographed_moves( + robot, + localizer, + object_ids, + base_moves, + hand_moves, + open_and_close_gripper=False, + allowed_regions=allowed_regions, + # FIXME need to pass in VLM predicates and id2object + vlm_predicates=None, + id2object=None, + ) def find_objects( From 5ebd44976703e13428357e8a964d285687a03a9b Mon Sep 17 00:00:00 2001 From: Linfeng Date: Sat, 11 May 2024 13:38:16 -0400 Subject: [PATCH 68/71] add print rich table for VLM atoms --- predicators/envs/spot_env.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 834cb05f18..4942b5fb2f 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -18,7 +18,10 @@ from bosdyn.client.sdk import Robot from bosdyn.client.util import authenticate, setup_logging from gym.spaces import Box +from predicators.utils import log_rich_table from scipy.spatial import Delaunay +from rich.table import Table +from rich import print from predicators import utils from predicators.envs import BaseEnv @@ -768,7 +771,29 @@ def _build_realworld_observation( vlm_atom_return[atom] = result # Logging - logging.info(f"Calculated VLM atoms (in current obs): {dict(vlm_atom_new)}") + # logging.info(f"Calculated VLM atoms (in current obs): {dict(vlm_atom_new)}") + # use Rich to print as table! + table = Table(title="Evaluated VLM atoms (in current obs)") + table.add_column("Atom", style="cyan") + table.add_column("Value", style="magenta") + for atom, result in vlm_atom_new.items(): + table.add_row(str(atom), str(result)) + logging.info(log_rich_table(table)) + + # Add table to show value in vlm_atom_new and vlm_atom_return to highlight how they change? + table_compare = Table(title="VLM atoms comparison") + table_compare.add_column("Atom", style="cyan") + table_compare.add_column("Value (Last)", style="blue") + table_compare.add_column("Value (New)", style="magenta") + vlm_atom_union = set(vlm_atom_new.keys()) | set(curr_obs.vlm_atom_dict.keys()) + for atom in vlm_atom_union: + table_compare.add_row( + str(atom), + str(curr_obs.vlm_atom_dict.get(atom, None)), + str(vlm_atom_new.get(atom, None)) + ) + logging.info(log_rich_table(table_compare)) + logging.info( f"True VLM atoms (after updated with current obs): " f"{dict(filter(lambda it: it[1], vlm_atom_return.items()))}" @@ -1576,7 +1601,7 @@ def _get_sweeping_surface_for_container(container: Object, _Inside = VLMPredicate( "Inside", [_movable_object_type, _container_type], prompt= - "This typically describes an object inside a container, so it's in conflict with the object being on a surface. Please check the image and confirm the object is inside the container." + "This typically describes an object inside a container (so it's overlapping), and it's in conflict with the object being on a surface. Please check the image and confirm the object is inside the container." ) _FakeInside = VLMPredicate( _Inside.name, From dc6ae7b3f997b590b036a625f4d3a6d8064d23f2 Mon Sep 17 00:00:00 2001 From: Linfeng Date: Sat, 11 May 2024 13:41:21 -0400 Subject: [PATCH 69/71] formatting --- predicators/envs/spot_env.py | 28 +++++++++---------- .../perception/object_perception.py | 4 ++- .../spot_utils/skills/spot_find_objects.py | 6 ++-- predicators/structs.py | 4 ++- predicators/utils.py | 9 +++--- 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 4942b5fb2f..6f2d3f7fbc 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -18,10 +18,9 @@ from bosdyn.client.sdk import Robot from bosdyn.client.util import authenticate, setup_logging from gym.spaces import Box -from predicators.utils import log_rich_table -from scipy.spatial import Delaunay -from rich.table import Table from rich import print +from rich.table import Table +from scipy.spatial import Delaunay from predicators import utils from predicators.envs import BaseEnv @@ -55,6 +54,7 @@ GroundAtom, LiftedAtom, Object, Observation, Predicate, \ SpotActionExtraInfo, State, STRIPSOperator, Type, Variable, \ VLMGroundAtom, VLMPredicate +from predicators.utils import log_rich_table ############################################################################### # Base Class # @@ -757,11 +757,11 @@ def _build_realworld_observation( objects = list(all_objects_in_view.keys()) vlm_atoms = get_vlm_atom_combinations(objects, vlm_predicates) vlm_atom_new: Dict[VLMGroundAtom, - bool or None] = vlm_predicate_batch_classify( - vlm_atoms, - rgbds, - predicates=vlm_predicates, - get_dict=True) + bool or None] = vlm_predicate_batch_classify( + vlm_atoms, + rgbds, + predicates=vlm_predicates, + get_dict=True) # Update VLM atom value if the new ground atom value is not None # Otherwise, use the value in current obs @@ -785,19 +785,17 @@ def _build_realworld_observation( table_compare.add_column("Atom", style="cyan") table_compare.add_column("Value (Last)", style="blue") table_compare.add_column("Value (New)", style="magenta") - vlm_atom_union = set(vlm_atom_new.keys()) | set(curr_obs.vlm_atom_dict.keys()) + vlm_atom_union = set(vlm_atom_new.keys()) | set( + curr_obs.vlm_atom_dict.keys()) for atom in vlm_atom_union: table_compare.add_row( - str(atom), - str(curr_obs.vlm_atom_dict.get(atom, None)), - str(vlm_atom_new.get(atom, None)) - ) + str(atom), str(curr_obs.vlm_atom_dict.get(atom, None)), + str(vlm_atom_new.get(atom, None))) logging.info(log_rich_table(table_compare)) logging.info( f"True VLM atoms (after updated with current obs): " - f"{dict(filter(lambda it: it[1], vlm_atom_return.items()))}" - ) + f"{dict(filter(lambda it: it[1], vlm_atom_return.items()))}") else: vlm_predicates = set() diff --git a/predicators/spot_utils/perception/object_perception.py b/predicators/spot_utils/perception/object_perception.py index 0ee8a5fb8b..66bd7549f7 100644 --- a/predicators/spot_utils/perception/object_perception.py +++ b/predicators/spot_utils/perception/object_perception.py @@ -190,7 +190,9 @@ def vlm_predicate_batch_classify( if len(queries) == 0: return {} - queries_print = [atom.get_query_str(include_prompt=False) for atom in atoms] + queries_print = [ + atom.get_query_str(include_prompt=False) for atom in atoms + ] logging.info(f"VLM predicate evaluation queries: {queries_print}") # Call VLM to evaluate the queries diff --git a/predicators/spot_utils/skills/spot_find_objects.py b/predicators/spot_utils/skills/spot_find_objects.py index ac0801228e..f6c1286767 100644 --- a/predicators/spot_utils/skills/spot_find_objects.py +++ b/predicators/spot_utils/skills/spot_find_objects.py @@ -120,10 +120,8 @@ def _find_objects_with_choreographed_moves( # Logging print(f"Calculated VLM atoms (in all views): {dict(all_vlm_atom_dict)}") - print( - f"True VLM atoms (in all views; with values as True): " - f"{dict(filter(lambda it: it[1], all_vlm_atom_dict.items()))}" - ) + print(f"True VLM atoms (in all views; with values as True): " + f"{dict(filter(lambda it: it[1], all_vlm_atom_dict.items()))}") table = Table(title="Evaluated VLM atoms (in all views)") table.add_column("Atom", style="cyan") diff --git a/predicators/structs.py b/predicators/structs.py index 688ab03f1a..ffc432bdf7 100644 --- a/predicators/structs.py +++ b/predicators/structs.py @@ -472,7 +472,9 @@ class VLMGroundAtom(GroundAtom): # NOTE: This subclasses GroundAtom to support VLM predicates and classifiers predicate: VLMPredicate - def get_query_str(self, without_type: bool = False, include_prompt: bool = True) -> str: + def get_query_str(self, + without_type: bool = False, + include_prompt: bool = True) -> str: """Get a query string for this ground atom. Instead of directly evaluating the ground atom, we will use the diff --git a/predicators/utils.py b/predicators/utils.py index 9964c9e5b7..5056de1f91 100644 --- a/predicators/utils.py +++ b/predicators/utils.py @@ -42,6 +42,7 @@ import matplotlib.pyplot as plt import numpy as np import pathos.multiprocessing as mp +import rich.table from bosdyn.client import math_helpers from gym.spaces import Box from matplotlib import patches @@ -49,10 +50,9 @@ from pyperplan.heuristics.heuristic_base import \ Heuristic as _PyperplanBaseHeuristic from pyperplan.planner import HEURISTICS as _PYPERPLAN_HEURISTICS -from scipy.stats import beta as BetaRV -import rich.table from rich.console import Console from rich.text import Text +from scipy.stats import beta as BetaRV from predicators.args import create_arg_parser from predicators.pybullet_helpers.joint import JointPositions @@ -3869,8 +3869,9 @@ def run_ground_nsrt_with_assertions(ground_nsrt: _GroundNSRT, def log_rich_table(rich_table: rich.table.Table) -> "Texssas": - """Generate an ascii formatted presentation of a Rich table - Eliminates any column styling + """Generate an ascii formatted presentation of a Rich table. + + Eliminates any column styling. """ console = Console(width=150) with console.capture() as capture: From 0e0d6cf815c8bee3f3eff435fccf39a082078f9e Mon Sep 17 00:00:00 2001 From: Linfeng Date: Sat, 18 May 2024 14:22:18 -0700 Subject: [PATCH 70/71] clean up previous legacy version of individual VLM classifier --- predicators/envs/spot_env.py | 78 ++++++++++-------------------------- 1 file changed, 22 insertions(+), 56 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 6f2d3f7fbc..3ff8005413 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -1196,36 +1196,23 @@ def _object_in_xy_classifier(state: State, def _on_classifier(state: State, objects: Sequence[Object]) -> bool: obj_on, obj_surface = objects - currently_visible = all([o in state.visible_objects for o in objects]) - # If object not all visible and choose to use VLM, - # then use predicate values of previous time step - if CFG.spot_vlm_eval_predicate and not currently_visible: - # TODO: add all previous atoms to the state - raise NotImplementedError - - # Call VLM to evaluate predicate value - elif CFG.spot_vlm_eval_predicate and currently_visible: - # predicate_str = f""" - # On({obj_on}, {obj_surface}) - # (Whether {obj_on} is on {obj_surface} in the image?) - # """ - # return vlm_predicate_classify(predicate_str, state) + # NOTE: Legacy version evaluate predicates individually + if CFG.spot_vlm_eval_predicate: raise RuntimeError( "VLM predicate classifier should be evaluated in batch!") - else: - # Check that the bottom of the object is close to the top of the surface. - expect = state.get(obj_surface, - "z") + state.get(obj_surface, "height") / 2 - actual = state.get(obj_on, "z") - state.get(obj_on, "height") / 2 - classification_val = abs(actual - expect) < _ONTOP_Z_THRESHOLD - - # If so, check that the object is within the bounds of the surface. - if not _object_in_xy_classifier( - state, obj_on, obj_surface, buffer=_ONTOP_SURFACE_BUFFER): - return False + # Check that the bottom of the object is close to the top of the surface. + expect = state.get(obj_surface, + "z") + state.get(obj_surface, "height") / 2 + actual = state.get(obj_on, "z") - state.get(obj_on, "height") / 2 + classification_val = abs(actual - expect) < _ONTOP_Z_THRESHOLD + + # If so, check that the object is within the bounds of the surface. + if not _object_in_xy_classifier( + state, obj_on, obj_surface, buffer=_ONTOP_SURFACE_BUFFER): + return False - return classification_val + return classification_val def _top_above_classifier(state: State, objects: Sequence[Object]) -> bool: @@ -1240,22 +1227,11 @@ def _top_above_classifier(state: State, objects: Sequence[Object]) -> bool: def _inside_classifier(state: State, objects: Sequence[Object]) -> bool: obj_in, obj_container = objects - # currently_visible = all([o in state.visible_objects for o in objects]) - # # If object not all visible and choose to use VLM, - # # then use predicate values of previous time step - # if CFG.spot_vlm_eval_predicate and not currently_visible: - # # TODO: add all previous atoms to the state - # raise NotImplementedError - # - # # Call VLM to evaluate predicate value - # elif CFG.spot_vlm_eval_predicate and currently_visible: - # predicate_str = f""" - # Inside({obj_in}, {obj_container}) - # (Whether {obj_in} is inside {obj_container} in the image?) - # """ - # return vlm_predicate_classify(predicate_str, state) - # - # else: + # NOTE: Legacy version evaluate predicates individually + if CFG.spot_vlm_eval_predicate: + raise RuntimeError( + "VLM predicate classifier should be evaluated in batch!") + if not _object_in_xy_classifier( state, obj_in, obj_container, buffer=_INSIDE_SURFACE_BUFFER): return False @@ -1369,20 +1345,10 @@ def _blocking_classifier(state: State, objects: Sequence[Object]) -> bool: if blocker_obj == blocked_obj: return False - # currently_visible = all([o in state.visible_objects for o in objects]) - # # If object not all visible and choose to use VLM, - # # then use predicate values of previous time step - # if CFG.spot_vlm_eval_predicate and not currently_visible: - # # TODO: add all previous atoms to the state - # raise NotImplementedError - # - # # Call VLM to evaluate predicate value - # elif CFG.spot_vlm_eval_predicate and currently_visible: - # predicate_str = f""" - # (Whether {blocker_obj} is blocking {blocked_obj} for further manipulation in the image?) - # Blocking({blocker_obj}, {blocked_obj}) - # """ - # return vlm_predicate_classify(predicate_str, state) + # NOTE: Legacy version evaluate predicates individually + if CFG.spot_vlm_eval_predicate: + raise RuntimeError( + "VLM predicate classifier should be evaluated in batch!") # Only consider draggable (non-placeable, movable) objects to be blockers. if not blocker_obj.is_instance(_movable_object_type): From 13f21cedc77589fd8d7fbd677f1c3e944dd667cd Mon Sep 17 00:00:00 2001 From: Linfeng Date: Sat, 18 May 2024 17:23:02 -0700 Subject: [PATCH 71/71] minor formatting --- predicators/envs/spot_env.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/predicators/envs/spot_env.py b/predicators/envs/spot_env.py index 3ff8005413..3492a6a3b8 100644 --- a/predicators/envs/spot_env.py +++ b/predicators/envs/spot_env.py @@ -1202,8 +1202,7 @@ def _on_classifier(state: State, objects: Sequence[Object]) -> bool: "VLM predicate classifier should be evaluated in batch!") # Check that the bottom of the object is close to the top of the surface. - expect = state.get(obj_surface, - "z") + state.get(obj_surface, "height") / 2 + expect = state.get(obj_surface, "z") + state.get(obj_surface, "height") / 2 actual = state.get(obj_on, "z") - state.get(obj_on, "height") / 2 classification_val = abs(actual - expect) < _ONTOP_Z_THRESHOLD