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.
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/CLAUDE.md +1 -1
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/PKG-INFO +1 -1
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/cortex_client.py +14 -5
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/model_type.py +65 -0
- grid_cortex_client-0.5.0/src/grid_cortex_client/models/contact_graspnet.py +155 -0
- grid_cortex_client-0.5.0/src/grid_cortex_client/models/graspgenx.py +151 -0
- grid_cortex_client-0.5.0/src/grid_cortex_client/models/metric3d.py +129 -0
- grid_cortex_client-0.5.0/tests/test_contact_graspnet.py +282 -0
- grid_cortex_client-0.5.0/tests/test_graspgenx.py +143 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_metric3d.py +20 -0
- grid_cortex_client-0.3.0/src/grid_cortex_client/models/metric3d.py +0 -95
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/.gitignore +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/README.md +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/pyproject.toml +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/__init__.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/client.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/cortex_hub_client.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/__init__.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/base_model.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/da3metric.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/fast_foundation_stereo.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/finetune_pi05.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/foundation_stereo.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/graspgen.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/gsam2.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/locate_anything.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/moondream.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/oneformer.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/owlv2.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/pi05.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/qwen_vl.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/sam2.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/sam3.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/ur5e_pyroki_collision_batch.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/ur5e_pyroki_fk.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/models/zoedepth.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/preprocessing.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/tools/__init__.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/tools/generate_enum.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/tools/registry.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/utils.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/ws.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/README.md +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/__init__.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/conftest.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_cortex_hub_client.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_da3metric.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_fast_foundation_stereo.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_finetune_pi05.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_foundation_stereo.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_graspgen.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_gsam2.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_locate_anything.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_moondream.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_oneformer.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_owlv2.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_pi05.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_qwen_vl.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_sam2.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_sam3.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_ur5e_pyroki_collision_batch.py +0 -0
- {grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/tests/test_ur5e_pyroki_fk.py +0 -0
- {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
|
|
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
|
|
{grid_cortex_client-0.3.0 → grid_cortex_client-0.5.0}/src/grid_cortex_client/cortex_client.py
RENAMED
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
)
|