grid-cortex-client 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/CLAUDE.md +1 -1
  2. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/PKG-INFO +1 -1
  3. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/cortex_client.py +14 -5
  4. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/model_type.py +65 -0
  5. grid_cortex_client-0.5.0/src/grid_cortex_client/models/contact_graspnet.py +155 -0
  6. grid_cortex_client-0.5.0/src/grid_cortex_client/models/graspgenx.py +151 -0
  7. grid_cortex_client-0.5.0/src/grid_cortex_client/models/metric3d.py +129 -0
  8. grid_cortex_client-0.5.0/tests/test_contact_graspnet.py +282 -0
  9. grid_cortex_client-0.5.0/tests/test_graspgenx.py +143 -0
  10. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_metric3d.py +20 -0
  11. grid_cortex_client-0.3.0/src/grid_cortex_client/models/metric3d.py +0 -95
  12. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/.gitignore +0 -0
  13. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/README.md +0 -0
  14. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/pyproject.toml +0 -0
  15. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/__init__.py +0 -0
  16. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/client.py +0 -0
  17. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/cortex_hub_client.py +0 -0
  18. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/__init__.py +0 -0
  19. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/base_model.py +0 -0
  20. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/da3metric.py +0 -0
  21. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/fast_foundation_stereo.py +0 -0
  22. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/finetune_pi05.py +0 -0
  23. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/foundation_stereo.py +0 -0
  24. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/graspgen.py +0 -0
  25. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/gsam2.py +0 -0
  26. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/locate_anything.py +0 -0
  27. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/moondream.py +0 -0
  28. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/oneformer.py +0 -0
  29. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/owlv2.py +0 -0
  30. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/pi05.py +0 -0
  31. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/qwen_vl.py +0 -0
  32. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/sam2.py +0 -0
  33. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/sam3.py +0 -0
  34. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/ur5e_pyroki_collision_batch.py +0 -0
  35. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/ur5e_pyroki_fk.py +0 -0
  36. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/zoedepth.py +0 -0
  37. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/preprocessing.py +0 -0
  38. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/tools/__init__.py +0 -0
  39. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/tools/generate_enum.py +0 -0
  40. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/tools/registry.py +0 -0
  41. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/utils.py +0 -0
  42. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/ws.py +0 -0
  43. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/README.md +0 -0
  44. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/__init__.py +0 -0
  45. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/conftest.py +0 -0
  46. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_cortex_hub_client.py +0 -0
  47. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_da3metric.py +0 -0
  48. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_fast_foundation_stereo.py +0 -0
  49. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_finetune_pi05.py +0 -0
  50. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_foundation_stereo.py +0 -0
  51. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_graspgen.py +0 -0
  52. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_gsam2.py +0 -0
  53. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_locate_anything.py +0 -0
  54. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_moondream.py +0 -0
  55. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_oneformer.py +0 -0
  56. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_owlv2.py +0 -0
  57. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_pi05.py +0 -0
  58. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_qwen_vl.py +0 -0
  59. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_sam2.py +0 -0
  60. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_sam3.py +0 -0
  61. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_ur5e_pyroki_collision_batch.py +0 -0
  62. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_ur5e_pyroki_fk.py +0 -0
  63. {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_zoedepth.py +0 -0
@@ -10,7 +10,7 @@ uv run --package grid-cortex-client pytest
10
10
 
11
11
  - Build tool: hatchling + hatch-vcs
12
12
  - Version: set via `BUILD_VERSION` env (auto-bumped on main from squash-commit type — see `knowledge/golden-rules/cortex/client-versioning.md`)
13
- - PyPI history: pre-PR-#44 versions up to `0.2.118` were published by the old pipeline; the new auto-pipeline starts at `0.3.0` and uses `cortex-client/v*` git tags as the version source of truth (first auto-publish lands via PR #45's `feat(cortex):` merge once its review feedback is addressed)
13
+ - PyPI history: pre-PR-#44 versions up to `0.2.118` were published by the old pipeline; the auto-pipeline (live since PR #45) starts at `0.3.0` and uses `cortex-client/v*` git tags as the version source of truth
14
14
 
15
15
  ## Key Details
16
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grid-cortex-client
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Python client for Grid Cortex
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.8
@@ -62,11 +62,20 @@ class _CortexClientBase:
62
62
  _MODEL_ID_TO_HANDLER_CLASS: Dict[str, Type[BaseModel]] = get_registry()
63
63
 
64
64
  def _get_handler_class(self, model_id: Union[str, ModelType]) -> Optional[Type[BaseModel]]:
65
- """Look up the handler class for a model ID."""
66
- model_id_str = model_id.value if isinstance(model_id, ModelType) else str(model_id)
67
- for keyword, HClass in self._MODEL_ID_TO_HANDLER_CLASS.items():
68
- if keyword in model_id_str.lower():
69
- return HClass
65
+ """Look up the handler class for a model ID.
66
+
67
+ Prefer an exact match so a registered id that is a substring of another
68
+ (e.g. ``graspgen`` vs ``graspgenx``) can't shadow it. Fall back to
69
+ substring matching, longest keyword first, to keep the lenient behavior
70
+ for free-form ids while still preventing short-id shadowing.
71
+ """
72
+ model_id_str = (model_id.value if isinstance(model_id, ModelType) else str(model_id)).lower()
73
+ exact = self._MODEL_ID_TO_HANDLER_CLASS.get(model_id_str)
74
+ if exact is not None:
75
+ return exact
76
+ for keyword in sorted(self._MODEL_ID_TO_HANDLER_CLASS, key=len, reverse=True):
77
+ if keyword in model_id_str:
78
+ return self._MODEL_ID_TO_HANDLER_CLASS[keyword]
70
79
  return None
71
80
 
72
81
  def _preprocess(
@@ -13,6 +13,44 @@ from enum import Enum
13
13
 
14
14
 
15
15
  class ModelType(Enum):
16
+ CONTACT_GRASPNET = "contact-graspnet"
17
+ """
18
+ Generate 6-DoF grasp poses from a depth image + camera intrinsics
19
+ (+ optional segmentation mask) using Contact-GraspNet (PyTorch port).
20
+
21
+ Args:
22
+ depth_image (Union[str, Image.Image, np.ndarray]): Depth image (path/URL/PIL/ndarray; float32 H×W).
23
+ camera_intrinsics (Union[str, np.ndarray]): 3×3 intrinsics matrix.
24
+ seg_image (Union[str, Image.Image, np.ndarray, None]): Optional segmentation mask (uint8 H×W).
25
+ When supplied + aux_args["filter_grasps"] = True, grasps are restricted to the masked region.
26
+ aux_args (Dict[str, Any]): Optional dict. Keys:
27
+ - "top_k" (int, default 64): cap on returned grasps after score-sort.
28
+ - "z_range" ([float, float], default [0.2, 1.8]): z-axis crop in metres.
29
+ - "local_regions" (bool, default False): crop 3D regions around segments.
30
+ - "filter_grasps" (bool, default False): restrict grasps to segmap; requires seg_image.
31
+ - "forward_passes" (int, default 1): how many forward passes to ensemble.
32
+ timeout (float | None): Optional HTTP timeout.
33
+
34
+ Returns:
35
+ Dict with:
36
+ - grasps: Array of 4×4 grasp poses in camera frame, shape (N, 4, 4), N <= top_k.
37
+ - confidence: Array of confidence scores in [0, 1], shape (N,).
38
+ - latency_ms: Optional server-reported latency in milliseconds.
39
+
40
+ Raises:
41
+ ValueError: If depth or intrinsics cannot be loaded.
42
+ RuntimeError: If no HTTP transport is configured.
43
+ Exception: If the HTTP request fails.
44
+
45
+ Example:
46
+ >>> from grid_cortex_client import CortexClient, ModelType
47
+ >>> import numpy as np
48
+ >>> client = CortexClient()
49
+ >>> depth = np.load("depth.npy")
50
+ >>> K = np.array([[615,0,320],[0,615,240],[0,0,1]], dtype=np.float64)
51
+ >>> out = client.run(ModelType.CONTACT_GRASPNET, depth_image=depth, camera_intrinsics=K, aux_args={"top_k": 32})
52
+ >>> print(out["grasps"].shape) # (<=32, 4, 4)
53
+ """
16
54
  DA3METRIC = "da3metric"
17
55
  """
18
56
  Estimate metric depth from single RGB image.
@@ -181,6 +219,33 @@ class ModelType(Enum):
181
219
  >>> grasps, conf = client.run(ModelType.GRASPGEN, depth_image=depth_image, seg_image=seg_image, camera_intrinsics=K, aux_args=aux)
182
220
  >>> print(f"Generated {len(grasps)} grasps")
183
221
  """
222
+ GRASPGENX = "graspgenx"
223
+ """
224
+ Generate 6-DoF grasps for a named gripper with GraspGenX.
225
+
226
+ Args:
227
+ aux_args (Dict[str, Any]): Must include "gripper_name"; optional
228
+ "num_grasps", "planner" ("graspmoe"|"diffusion"), "grasp_threshold",
229
+ "camera_extrinsics".
230
+ point_cloud (Union[str, np.ndarray, Sequence, None]): (N, 3) object point cloud.
231
+ depth_image / seg_image / camera_intrinsics: alternative to point_cloud.
232
+ timeout (float | None): Optional HTTP timeout.
233
+
234
+ Returns:
235
+ Dict[str, Any]: Dict with:
236
+ - grasps: (N, 4, 4) array of grasp poses
237
+ - confidence: (N,) array of scores
238
+ - latency_ms: server-reported latency
239
+
240
+ Examples:
241
+ >>> from grid_cortex_client import CortexClient, ModelType
242
+ >>> import numpy as np
243
+ >>> client = CortexClient()
244
+ >>> pc = np.load("object_pc.npy") # (N, 3)
245
+ >>> out = client.run(ModelType.GRASPGENX, point_cloud=pc,
246
+ ... aux_args={"gripper_name": "robotiq_2f_85", "num_grasps": 64})
247
+ >>> print(out["grasps"].shape)
248
+ """
184
249
  GSAM2 = "gsam2"
185
250
  """
186
251
  Segment objects in image using text prompt.
@@ -0,0 +1,155 @@
1
+ # grid_cortex_client/src/grid_cortex_client/models/contact_graspnet.py
2
+ """ContactGraspNet wrapper.
3
+
4
+ 6-DoF grasp pose generation from a depth image + camera intrinsics, with an
5
+ optional segmentation mask to restrict the working region.
6
+
7
+ Successor to the legacy `graspnet` (TF) model; runs on the PyTorch port via
8
+ the cortex `contact_graspnet` Ray Serve deployment. Wire format mirrors
9
+ `graspgen` so swapping models in client code is just changing the ModelType.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Dict, Union
15
+
16
+ import numpy as np
17
+ from PIL import Image
18
+
19
+ from ..preprocessing import array_to_npy_bytes, load_image
20
+ from .base_model import BaseModel
21
+
22
+
23
+ class ContactGraspNet(BaseModel):
24
+ """6-DoF grasp generation from depth + intrinsics (+ optional seg).
25
+
26
+ Preferred usage
27
+ ---------------
28
+ ```pycon
29
+ >>> grasps, conf = CortexClient().run(
30
+ ... "contact_graspnet",
31
+ ... depth_image=depth, camera_intrinsics=K,
32
+ ... seg_image=seg, aux_args={"top_k": 64}
33
+ ... )
34
+ ```
35
+ """
36
+
37
+ name: str = "contact-graspnet"
38
+ model_id: str = "contact-graspnet"
39
+
40
+ def preprocess(
41
+ self,
42
+ *,
43
+ depth_image: Union[str, Image.Image, np.ndarray, None] = None,
44
+ camera_intrinsics: Union[str, np.ndarray, None] = None,
45
+ seg_image: Union[str, Image.Image, np.ndarray, None] = None,
46
+ aux_args: Dict[str, Any] | None = None,
47
+ ) -> Dict[str, Any]:
48
+ """Encode depth + intrinsics (+ optional seg) for the server.
49
+
50
+ Args:
51
+ depth_image: Depth image (path/URL/PIL/ndarray; float32 H×W).
52
+ camera_intrinsics: 3×3 intrinsics matrix (path/ndarray).
53
+ seg_image: Optional segmentation mask (path/PIL/ndarray; uint8 H×W).
54
+ When supplied + ``aux_args["filter_grasps"] = True``, grasps are
55
+ restricted to the masked region (see model README for details).
56
+ aux_args: Optional dict with ``top_k`` (default 64), ``z_range``
57
+ (default [0.2, 1.8]), ``local_regions``, ``filter_grasps``,
58
+ ``forward_passes``.
59
+ """
60
+ if depth_image is None or camera_intrinsics is None:
61
+ raise ValueError("depth_image and camera_intrinsics are required.")
62
+
63
+ # Accept .npy paths in addition to image formats. PIL.Image.open chokes
64
+ # on raw NumPy files; np.load is the right call for them. .png/.exr
65
+ # depth maps still flow through load_image as before.
66
+ if isinstance(depth_image, str):
67
+ if depth_image.endswith(".npy"):
68
+ depth_array = np.load(depth_image)
69
+ else:
70
+ depth_array = np.array(load_image(depth_image))
71
+ elif isinstance(depth_image, Image.Image):
72
+ depth_array = np.array(load_image(depth_image))
73
+ else:
74
+ depth_array = np.asarray(depth_image)
75
+
76
+ if isinstance(camera_intrinsics, str):
77
+ intrinsics = np.load(camera_intrinsics)
78
+ else:
79
+ intrinsics = np.asarray(camera_intrinsics)
80
+
81
+ payload: Dict[str, Any] = {
82
+ "depth_image": array_to_npy_bytes(depth_array.astype(np.float32)),
83
+ "camera_intrinsics": array_to_npy_bytes(intrinsics.astype(np.float64)),
84
+ "aux_args": aux_args or {},
85
+ }
86
+
87
+ if seg_image is not None:
88
+ if isinstance(seg_image, (str, Image.Image)):
89
+ seg_array = np.array(load_image(seg_image))
90
+ else:
91
+ seg_array = np.asarray(seg_image)
92
+ payload["seg_image"] = array_to_npy_bytes(seg_array.astype(np.uint8))
93
+
94
+ return payload
95
+
96
+ def postprocess(self, response_data: Dict[str, Any], **_: Any) -> Dict[str, Any]:
97
+ """Decode grasps and confidence from response."""
98
+ raw_grasps = response_data["output"]
99
+ # Keep the (N, 4, 4) shape invariant when no grasps were found —
100
+ # np.array([]) → (0,) which breaks any caller indexing grasps[:, :3, 3].
101
+ if not raw_grasps:
102
+ grasps = np.zeros((0, 4, 4), dtype=np.float32)
103
+ else:
104
+ grasps = np.array(raw_grasps)
105
+ conf = np.array(response_data["confidence"])
106
+ return {
107
+ "grasps": grasps,
108
+ "confidence": conf,
109
+ "latency_ms": response_data.get("latency_ms"),
110
+ }
111
+
112
+ def run(
113
+ self,
114
+ *,
115
+ depth_image: Union[str, Image.Image, np.ndarray, None] = None,
116
+ camera_intrinsics: Union[str, np.ndarray, None] = None,
117
+ seg_image: Union[str, Image.Image, np.ndarray, None] = None,
118
+ aux_args: Dict[str, Any] | None = None,
119
+ timeout: float | None = None,
120
+ ) -> Dict[str, Any]:
121
+ """Generate grasps using ContactGraspNet.
122
+
123
+ Args:
124
+ depth_image: Depth image (required).
125
+ camera_intrinsics: 3×3 intrinsics matrix (required).
126
+ seg_image: Optional segmentation mask (uint8 H×W).
127
+ aux_args: Optional dict. Keys: ``top_k`` (int, default 64),
128
+ ``z_range`` ([float, float], default [0.2, 1.8]),
129
+ ``local_regions`` (bool, default False),
130
+ ``filter_grasps`` (bool, default False; requires ``seg_image``),
131
+ ``forward_passes`` (int, default 1).
132
+ timeout: Optional HTTP timeout.
133
+
134
+ Returns:
135
+ Dict with:
136
+ - ``grasps``: (N, 4, 4) array of grasp poses in camera frame
137
+ - ``confidence``: (N,) array of confidence scores in [0, 1]
138
+ - ``latency_ms``: server-reported latency
139
+
140
+ Examples:
141
+ >>> from grid_cortex_client import CortexClient, ModelType
142
+ >>> import numpy as np
143
+ >>> client = CortexClient()
144
+ >>> depth = np.load("depth.npy")
145
+ >>> K = np.array([[615,0,320],[0,615,240],[0,0,1]], dtype=np.float64)
146
+ >>> out = client.run(ModelType.CONTACT_GRASPNET, depth_image=depth, camera_intrinsics=K, aux_args={"top_k": 32})
147
+ >>> print(out["grasps"].shape) # (<=32, 4, 4)
148
+ """
149
+ return super().run(
150
+ depth_image=depth_image,
151
+ camera_intrinsics=camera_intrinsics,
152
+ seg_image=seg_image,
153
+ aux_args=aux_args,
154
+ timeout=timeout,
155
+ )
@@ -0,0 +1,151 @@
1
+ # grid_cortex_client/src/grid_cortex_client/models/graspgenx.py
2
+ """GraspGenX wrapper.
3
+
4
+ Cross-embodiment 6-DoF grasp generation (NVlabs/GraspGenX). A single model
5
+ conditioned on a gripper representation, so grasps are requested for a named
6
+ gripper (``gripper_name``) rather than a per-gripper model.
7
+
8
+ Input: a segmented object point cloud (or depth+seg+intrinsics, converted
9
+ server-side). Output: 6-DoF grasp poses + confidences.
10
+
11
+ NOTE: weights are under the NVIDIA Open Model License — see the model recipe.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, Dict, Sequence, Union
17
+
18
+ import numpy as np
19
+ from PIL import Image
20
+
21
+ from ..preprocessing import array_to_npy_bytes, load_image
22
+ from .base_model import BaseModel
23
+
24
+
25
+ class GraspGenX(BaseModel):
26
+ """Cross-embodiment grasp generation (GraspGenX).
27
+
28
+ Preferred usage
29
+ ---------------
30
+ ```pycon
31
+ >>> out = CortexClient().run(
32
+ ... "graspgenx",
33
+ ... point_cloud=pc, # (N, 3) float32
34
+ ... aux_args={"gripper_name": "robotiq_2f_85", "num_grasps": 64},
35
+ ... )
36
+ >>> grasps, conf = out["grasps"], out["confidence"]
37
+ ```
38
+ """
39
+
40
+ name: str = "graspgenx"
41
+ model_id: str = "graspgenx"
42
+
43
+ # ------------------------------------------------------------------
44
+ # BaseModel implementation
45
+ # ------------------------------------------------------------------
46
+
47
+ def preprocess(
48
+ self,
49
+ *,
50
+ aux_args: Dict[str, Any] | None = None,
51
+ point_cloud: Union[str, np.ndarray, Sequence[Sequence[float]], None] = None,
52
+ depth_image: Union[str, Image.Image, np.ndarray, None] = None,
53
+ seg_image: Union[str, Image.Image, np.ndarray, None] = None,
54
+ camera_intrinsics: Union[str, np.ndarray, None] = None,
55
+ ) -> Dict[str, Any]:
56
+ """Prepare the msgpack payload.
57
+
58
+ Args:
59
+ aux_args: Dict with "gripper_name", "num_grasps", "planner",
60
+ "grasp_threshold", "camera_extrinsics".
61
+ point_cloud: (N, 3) point cloud (ndarray/list/path to .npy).
62
+ depth_image / seg_image / camera_intrinsics: alternative to point_cloud;
63
+ converted to a point cloud server-side.
64
+ """
65
+ if aux_args is None:
66
+ raise ValueError("'aux_args' is required (must include 'gripper_name').")
67
+
68
+ has_pc = point_cloud is not None
69
+ has_depth_triplet = any(x is not None for x in (depth_image, seg_image, camera_intrinsics))
70
+ if has_pc and has_depth_triplet:
71
+ raise ValueError("Provide either point_cloud or depth_image+seg_image+camera_intrinsics, not both.")
72
+ if not has_pc and not all(x is not None for x in (depth_image, seg_image, camera_intrinsics)):
73
+ raise ValueError("Provide point_cloud, or all of depth_image + seg_image + camera_intrinsics.")
74
+
75
+ if has_pc:
76
+ pc_array = np.load(point_cloud) if isinstance(point_cloud, str) else np.asarray(point_cloud)
77
+ if pc_array.ndim != 2 or pc_array.shape[1] != 3:
78
+ raise ValueError("point_cloud must be an (N, 3) array.")
79
+ return {"point_cloud": array_to_npy_bytes(pc_array.astype(np.float32)), "aux_args": aux_args}
80
+
81
+ depth_array = (
82
+ np.array(load_image(depth_image))
83
+ if isinstance(depth_image, (str, Image.Image))
84
+ else np.asarray(depth_image)
85
+ )
86
+ seg_array = (
87
+ np.array(load_image(seg_image)) if isinstance(seg_image, (str, Image.Image)) else np.asarray(seg_image)
88
+ )
89
+ intrinsics = np.load(camera_intrinsics) if isinstance(camera_intrinsics, str) else np.asarray(camera_intrinsics)
90
+ return {
91
+ "depth_image": array_to_npy_bytes(depth_array),
92
+ "seg_image": array_to_npy_bytes(seg_array),
93
+ "camera_intrinsics": array_to_npy_bytes(intrinsics),
94
+ "aux_args": aux_args,
95
+ }
96
+
97
+ def postprocess(self, response_data: Dict[str, Any], **_: Any) -> Dict[str, Any]: # noqa: D401
98
+ """Decode grasps + confidence from the response."""
99
+ grasps = np.array(response_data["output"])
100
+ if grasps.size == 0:
101
+ # Keep the documented (N, 4, 4) contract on the empty path.
102
+ grasps = np.empty((0, 4, 4), dtype=np.float32)
103
+ return {
104
+ "grasps": grasps,
105
+ "confidence": np.array(response_data["confidence"]),
106
+ "latency_ms": response_data.get("latency_ms"),
107
+ }
108
+
109
+ def run(
110
+ self,
111
+ *,
112
+ aux_args: Dict[str, Any],
113
+ point_cloud: Union[str, np.ndarray, Sequence[Sequence[float]], None] = None,
114
+ depth_image: Union[str, Image.Image, np.ndarray, None] = None,
115
+ seg_image: Union[str, Image.Image, np.ndarray, None] = None,
116
+ camera_intrinsics: Union[str, np.ndarray, None] = None,
117
+ timeout: float | None = None,
118
+ ) -> Dict[str, Any]:
119
+ """Generate 6-DoF grasps for a named gripper with GraspGenX.
120
+
121
+ Args:
122
+ aux_args (Dict[str, Any]): Must include "gripper_name"; optional
123
+ "num_grasps", "planner" ("graspmoe"|"diffusion"), "grasp_threshold",
124
+ "camera_extrinsics".
125
+ point_cloud (Union[str, np.ndarray, Sequence, None]): (N, 3) object point cloud.
126
+ depth_image / seg_image / camera_intrinsics: alternative to point_cloud.
127
+ timeout (float | None): Optional HTTP timeout.
128
+
129
+ Returns:
130
+ Dict[str, Any]: Dict with:
131
+ - grasps: (N, 4, 4) array of grasp poses
132
+ - confidence: (N,) array of scores
133
+ - latency_ms: server-reported latency
134
+
135
+ Examples:
136
+ >>> from grid_cortex_client import CortexClient, ModelType
137
+ >>> import numpy as np
138
+ >>> client = CortexClient()
139
+ >>> pc = np.load("object_pc.npy") # (N, 3)
140
+ >>> out = client.run(ModelType.GRASPGENX, point_cloud=pc,
141
+ ... aux_args={"gripper_name": "robotiq_2f_85", "num_grasps": 64})
142
+ >>> print(out["grasps"].shape)
143
+ """
144
+ return super().run(
145
+ aux_args=aux_args,
146
+ point_cloud=point_cloud,
147
+ depth_image=depth_image,
148
+ seg_image=seg_image,
149
+ camera_intrinsics=camera_intrinsics,
150
+ timeout=timeout,
151
+ )
@@ -0,0 +1,129 @@
1
+ """Metric3D wrapper.
2
+
3
+ Client-side helper for monocular metric depth estimation and surface normal
4
+ prediction powered by Metric3D ViT-Large.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from io import BytesIO
10
+ from typing import Any, Dict, Optional, Sequence, Union
11
+
12
+ import numpy as np
13
+ from PIL import Image
14
+
15
+ from ..preprocessing import load_image, image_to_bytes
16
+ from .base_model import BaseModel
17
+
18
+ VALID_OUTPUTS = ("depth", "normals")
19
+
20
+
21
+ class Metric3D(BaseModel):
22
+ """Monocular metric depth + surface normals (Metric3D ViT-Large).
23
+
24
+ Preferred usage (through :class:`CortexClient`)
25
+ ------------------------------------------------
26
+ ```pycon
27
+ >>> result = CortexClient().run("metric3d", image_input=img)
28
+ >>> result["depth"].shape # (H, W) float32 metres
29
+ >>> result["normals"].shape # (H, W, 3) float32 unit normals
30
+
31
+ >>> # Request a single map to cut response size (~75% smaller for depth-only)
32
+ >>> result = CortexClient().run("metric3d", image_input=img, outputs=["depth"])
33
+ >>> "normals" in result
34
+ False
35
+ ```
36
+ """
37
+
38
+ name: str = "metric3d"
39
+ model_id: str = "metric3d"
40
+
41
+ def __init__(self, *, transport=None) -> None:
42
+ super().__init__(transport=transport)
43
+
44
+ # ------------------------------------------------------------------
45
+ # BaseModel interface
46
+ # ------------------------------------------------------------------
47
+
48
+ def preprocess(
49
+ self,
50
+ *,
51
+ image_input: Union[str, Image.Image, np.ndarray],
52
+ outputs: Optional[Union[str, Sequence[str]]] = None,
53
+ ) -> Dict[str, Any]:
54
+ """Return msgpack-ready payload with raw image bytes under key ``image_input``.
55
+
56
+ Args:
57
+ image_input: RGB image as file path, URL, PIL Image, or numpy array.
58
+ outputs: Optional selection of maps to return — ``"depth"``,
59
+ ``"normals"``, or a non-empty subset of both. ``None`` (default)
60
+ returns both.
61
+
62
+ Raises:
63
+ ValueError: If ``outputs`` is empty or contains an unknown name.
64
+ """
65
+ pil_img = load_image(image_input)
66
+ payload: Dict[str, Any] = {"image_input": image_to_bytes(pil_img, encoding_format="JPEG")}
67
+
68
+ if outputs is not None:
69
+ selected = [outputs] if isinstance(outputs, str) else [str(o) for o in outputs]
70
+ if not selected or any(o not in VALID_OUTPUTS for o in selected):
71
+ raise ValueError(f"'outputs' must be a non-empty subset of {VALID_OUTPUTS}, got {outputs!r}")
72
+ payload["outputs"] = selected
73
+
74
+ return payload
75
+
76
+ def postprocess(
77
+ self,
78
+ response_data: Dict[str, Any],
79
+ **_: Any,
80
+ ) -> Dict[str, np.ndarray]:
81
+ """Decode the maps present in the server response.
82
+
83
+ Returns:
84
+ Dict with the returned subset of ``"depth"`` (H, W) float32 and
85
+ ``"normals"`` (H, W, 3) float32.
86
+ """
87
+ if isinstance(response_data, list):
88
+ response_data = response_data[0]
89
+
90
+ result: Dict[str, np.ndarray] = {}
91
+ for key in VALID_OUTPUTS:
92
+ buf = response_data.get(key)
93
+ if buf is None:
94
+ continue
95
+ if not isinstance(buf, (bytes, bytearray, memoryview)):
96
+ raise ValueError(f"'{key}' field is not bytes")
97
+ result[key] = np.load(BytesIO(buf), allow_pickle=False).astype(np.float32)
98
+
99
+ if not result:
100
+ raise ValueError(f"response contains none of {VALID_OUTPUTS}")
101
+ return result
102
+
103
+ def run(
104
+ self,
105
+ image_input: Union[str, Image.Image, np.ndarray],
106
+ timeout: float | None = None,
107
+ *,
108
+ outputs: Optional[Union[str, Sequence[str]]] = None,
109
+ ) -> Dict[str, np.ndarray]:
110
+ """Estimate metric depth and/or surface normals from a single RGB image.
111
+
112
+ Args:
113
+ image_input: RGB image as file path, URL, PIL Image, or numpy array.
114
+ timeout: Optional timeout in seconds for the HTTP request.
115
+ outputs: Keyword-only. Optional selection of maps to return —
116
+ ``"depth"``, ``"normals"``, or a non-empty subset of both.
117
+ ``None`` (default) returns both. Requesting only what you need
118
+ shrinks the response payload (normals are 3x the size of depth).
119
+
120
+ Returns:
121
+ Dict with the requested subset of:
122
+ - ``"depth"``: (H, W) float32 array, metric depth in metres.
123
+ - ``"normals"``: (H, W, 3) float32 array, unit surface normals.
124
+ """
125
+ return super().run(
126
+ image_input=image_input,
127
+ outputs=outputs,
128
+ timeout=timeout,
129
+ )