orient_express 2.3.1__tar.gz → 2.4.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 (22) hide show
  1. {orient_express-2.3.1 → orient_express-2.4.0}/PKG-INFO +26 -18
  2. {orient_express-2.3.1 → orient_express-2.4.0}/README.md +25 -17
  3. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/predictors/__init__.py +1 -0
  4. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/predictors/predictor.py +1 -1
  5. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/predictors/vector_index.py +37 -24
  6. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/vertex.py +18 -11
  7. {orient_express-2.3.1 → orient_express-2.4.0}/pyproject.toml +1 -1
  8. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/__init__.py +0 -0
  9. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/deployment.py +0 -0
  10. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/model_wrapper.py +0 -0
  11. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/predictors/classification.py +0 -0
  12. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/predictors/feature_extraction.py +0 -0
  13. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/predictors/instance_segmentation.py +0 -0
  14. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/predictors/multi_label_classification.py +0 -0
  15. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/predictors/object_detection.py +0 -0
  16. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/predictors/semantic_segmentation.py +0 -0
  17. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/sklearn_pipeline.py +0 -0
  18. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/utils/colors.py +0 -0
  19. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/utils/gs.py +0 -0
  20. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/utils/image_processor.py +0 -0
  21. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/utils/paths.py +0 -0
  22. {orient_express-2.3.1 → orient_express-2.4.0}/orient_express/utils/retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orient_express
3
- Version: 2.3.1
3
+ Version: 2.4.0
4
4
  Summary: A library to simplify model deployment to Vertex AI
5
5
  Author: Alexey Zankevich
6
6
  Author-email: alex.zankevich@shiftsmart.com
@@ -46,11 +46,13 @@ pip install orient_express
46
46
  ```
47
47
 
48
48
  For local development:
49
+
49
50
  ```bash
50
51
  pip install -e .
51
52
  ```
52
53
 
53
54
  Or with Poetry:
55
+
54
56
  ```bash
55
57
  poetry install
56
58
  ```
@@ -102,8 +104,8 @@ vertex_model.deploy_to_endpoint(
102
104
 
103
105
  # remote prediction API depends on the endpoint container deployed with the model
104
106
  predictions = vertex_model.remote_predict(
105
- [{"image": "https://storage.googleapis.com/ssm-media-uploads/example.jpg"}],
106
- endpoint_name="my-classifier-endpoint"
107
+ "my-classifier-endpoint",
108
+ [{"image": "https://storage.googleapis.com/ssm-media-uploads/example.jpg"}],
107
109
  )
108
110
  ```
109
111
 
@@ -179,19 +181,20 @@ predictions = local_predictor.predict(X_test)
179
181
  ### Platform Support Matrix
180
182
 
181
183
  | Platform | Architecture | ONNX Runtime Package | CUDA Available |
182
- |----------|--------------|---------------------|----------------|
183
- | Linux | x86_64 | onnxruntime-gpu | Yes |
184
- | Linux | aarch64 | onnxruntime | No |
185
- | Windows | x64 (AMD64) | onnxruntime-gpu | Yes |
186
- | Windows | ARM64 | onnxruntime | No |
187
- | macOS | x86_64 | onnxruntime | No |
188
- | macOS | arm64 | onnxruntime | No |
184
+ | -------- | ------------ | -------------------- | -------------- |
185
+ | Linux | x86_64 | onnxruntime-gpu | Yes |
186
+ | Linux | aarch64 | onnxruntime | No |
187
+ | Windows | x64 (AMD64) | onnxruntime-gpu | Yes |
188
+ | Windows | ARM64 | onnxruntime | No |
189
+ | macOS | x86_64 | onnxruntime | No |
190
+ | macOS | arm64 | onnxruntime | No |
189
191
 
190
192
  The appropriate package is installed automatically based on your platform.
191
193
 
192
194
  ### Selecting CPU vs CUDA Execution
193
195
 
194
196
  When loading a predictor, use the `device` parameter to specify the execution provider:
197
+
195
198
  ```python
196
199
  from orient_express.predictors import ObjectDetectionPredictor
197
200
 
@@ -203,6 +206,7 @@ predictor = ObjectDetectionPredictor("/path/to/model", classes, device="cuda")
203
206
  ```
204
207
 
205
208
  When using a Vertex AI model:
209
+
206
210
  ```python
207
211
  # CPU inference
208
212
  predictor = model.get_local_predictor(device="cpu")
@@ -540,11 +544,8 @@ index = build_vector_index(
540
544
  num_workers=8, # parallel image loading
541
545
  )
542
546
 
543
- # Aggregate to one centroid per label
544
- centroid_index = index.aggregate()
545
-
546
547
  # Save and load
547
- centroid_index.dump("/path/to/artifact_dir")
548
+ index.dump("/path/to/artifact_dir")
548
549
 
549
550
  from orient_express.predictors import get_predictor
550
551
  loaded_index = get_predictor("/path/to/artifact_dir")
@@ -560,16 +561,23 @@ batch_results = loaded_index.search_batch(query_matrix, k=5)
560
561
 
561
562
  #### Multi-label support
562
563
 
563
- Vectors can have multiple labels. This is useful when a single visual cluster maps to multiple SKUs:
564
+ Vectors can have composite labels (use tuples). This is useful when a single visual cluster maps to multiple things:
564
565
 
565
566
  ```python
566
567
  index = VectorIndex(
567
568
  vectors=feature_matrix,
568
- labels=[["sku_101", "sku_102"], ["sku_103"], ["sku_104", "sku_105"]],
569
+ labels=[("sku_101", "sku_102"), ("sku_103",)],
569
570
  )
571
+ ```
572
+
573
+ #### Per-label aggregation
570
574
 
571
- # aggregate() computes one centroid per unique label
572
- aggregated = index.aggregate() # 5 vectors, one per SKU
575
+ Vector indices in which labels are not unique can be aggregated so that each label has a single centroid.
576
+ If `per_label=True` and the labels are composite (tuples), then the labels will be unpacked and aggregated separately.
577
+
578
+ ```python
579
+ aggregated = index.aggregate(per_label=True) # 3 vectors, one per label element ["sku_101", "sku_102", "sku_103"]
580
+ aggregated = index.aggregate(per_label=False) # 2 vectors, one per composite label [("sku_101", "sku_102"), ("sku_103")]
573
581
  ```
574
582
 
575
583
  #### Output Structure
@@ -19,11 +19,13 @@ pip install orient_express
19
19
  ```
20
20
 
21
21
  For local development:
22
+
22
23
  ```bash
23
24
  pip install -e .
24
25
  ```
25
26
 
26
27
  Or with Poetry:
28
+
27
29
  ```bash
28
30
  poetry install
29
31
  ```
@@ -75,8 +77,8 @@ vertex_model.deploy_to_endpoint(
75
77
 
76
78
  # remote prediction API depends on the endpoint container deployed with the model
77
79
  predictions = vertex_model.remote_predict(
78
- [{"image": "https://storage.googleapis.com/ssm-media-uploads/example.jpg"}],
79
- endpoint_name="my-classifier-endpoint"
80
+ "my-classifier-endpoint",
81
+ [{"image": "https://storage.googleapis.com/ssm-media-uploads/example.jpg"}],
80
82
  )
81
83
  ```
82
84
 
@@ -152,19 +154,20 @@ predictions = local_predictor.predict(X_test)
152
154
  ### Platform Support Matrix
153
155
 
154
156
  | Platform | Architecture | ONNX Runtime Package | CUDA Available |
155
- |----------|--------------|---------------------|----------------|
156
- | Linux | x86_64 | onnxruntime-gpu | Yes |
157
- | Linux | aarch64 | onnxruntime | No |
158
- | Windows | x64 (AMD64) | onnxruntime-gpu | Yes |
159
- | Windows | ARM64 | onnxruntime | No |
160
- | macOS | x86_64 | onnxruntime | No |
161
- | macOS | arm64 | onnxruntime | No |
157
+ | -------- | ------------ | -------------------- | -------------- |
158
+ | Linux | x86_64 | onnxruntime-gpu | Yes |
159
+ | Linux | aarch64 | onnxruntime | No |
160
+ | Windows | x64 (AMD64) | onnxruntime-gpu | Yes |
161
+ | Windows | ARM64 | onnxruntime | No |
162
+ | macOS | x86_64 | onnxruntime | No |
163
+ | macOS | arm64 | onnxruntime | No |
162
164
 
163
165
  The appropriate package is installed automatically based on your platform.
164
166
 
165
167
  ### Selecting CPU vs CUDA Execution
166
168
 
167
169
  When loading a predictor, use the `device` parameter to specify the execution provider:
170
+
168
171
  ```python
169
172
  from orient_express.predictors import ObjectDetectionPredictor
170
173
 
@@ -176,6 +179,7 @@ predictor = ObjectDetectionPredictor("/path/to/model", classes, device="cuda")
176
179
  ```
177
180
 
178
181
  When using a Vertex AI model:
182
+
179
183
  ```python
180
184
  # CPU inference
181
185
  predictor = model.get_local_predictor(device="cpu")
@@ -513,11 +517,8 @@ index = build_vector_index(
513
517
  num_workers=8, # parallel image loading
514
518
  )
515
519
 
516
- # Aggregate to one centroid per label
517
- centroid_index = index.aggregate()
518
-
519
520
  # Save and load
520
- centroid_index.dump("/path/to/artifact_dir")
521
+ index.dump("/path/to/artifact_dir")
521
522
 
522
523
  from orient_express.predictors import get_predictor
523
524
  loaded_index = get_predictor("/path/to/artifact_dir")
@@ -533,16 +534,23 @@ batch_results = loaded_index.search_batch(query_matrix, k=5)
533
534
 
534
535
  #### Multi-label support
535
536
 
536
- Vectors can have multiple labels. This is useful when a single visual cluster maps to multiple SKUs:
537
+ Vectors can have composite labels (use tuples). This is useful when a single visual cluster maps to multiple things:
537
538
 
538
539
  ```python
539
540
  index = VectorIndex(
540
541
  vectors=feature_matrix,
541
- labels=[["sku_101", "sku_102"], ["sku_103"], ["sku_104", "sku_105"]],
542
+ labels=[("sku_101", "sku_102"), ("sku_103",)],
542
543
  )
544
+ ```
545
+
546
+ #### Per-label aggregation
543
547
 
544
- # aggregate() computes one centroid per unique label
545
- aggregated = index.aggregate() # 5 vectors, one per SKU
548
+ Vector indices in which labels are not unique can be aggregated so that each label has a single centroid.
549
+ If `per_label=True` and the labels are composite (tuples), then the labels will be unpacked and aggregated separately.
550
+
551
+ ```python
552
+ aggregated = index.aggregate(per_label=True) # 3 vectors, one per label element ["sku_101", "sku_102", "sku_103"]
553
+ aggregated = index.aggregate(per_label=False) # 2 vectors, one per composite label [("sku_101", "sku_102"), ("sku_103")]
546
554
  ```
547
555
 
548
556
  #### Output Structure
@@ -99,6 +99,7 @@ def load_vector_index(dir: str, metadata: dict | None = None):
99
99
  data = np.load(artifact_path, allow_pickle=False)
100
100
  vectors = data["vectors"]
101
101
  labels = json.loads(str(data["labels_json"]))
102
+ labels = [tuple(label) if isinstance(label, list) else label for label in labels]
102
103
  index = VectorIndex(vectors=vectors, labels=labels)
103
104
  index.model_path = artifact_path
104
105
  return index
@@ -46,7 +46,7 @@ class ImagePredictor(Predictor):
46
46
  self.model_path = model_path
47
47
 
48
48
  def get_serving_container_image_uri(self):
49
- return "us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:v2.1.2"
49
+ return "us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:v2.4.0"
50
50
 
51
51
  def get_serving_container_health_route(self, model_name):
52
52
  return f"/v1/models/{model_name}"
@@ -2,6 +2,7 @@ import os
2
2
  import json
3
3
  from dataclasses import dataclass
4
4
  import warnings
5
+ from typing import Any
5
6
 
6
7
  import yaml
7
8
  import numpy as np
@@ -37,7 +38,7 @@ class VectorIndex(Predictor):
37
38
  def __init__(
38
39
  self,
39
40
  vectors: np.ndarray,
40
- labels: list[list],
41
+ labels: list[int | str | tuple],
41
42
  normalize: bool = False,
42
43
  ):
43
44
  if vectors.ndim != 2:
@@ -53,8 +54,16 @@ class VectorIndex(Predictor):
53
54
  # Avoid division by zero for zero vectors
54
55
  norms = np.where(norms == 0, 1, norms)
55
56
  vectors = vectors / norms
57
+
56
58
  self.vectors = vectors
57
59
  self.labels = labels
60
+ self.label_to_idx = {}
61
+
62
+ for i, label in enumerate(labels):
63
+ if label not in self.label_to_idx:
64
+ self.label_to_idx[label] = []
65
+ self.label_to_idx[label].append(i)
66
+
58
67
  self.model_path = None
59
68
 
60
69
  def search(self, query: np.ndarray, k: int = 1) -> list[SearchResult]:
@@ -86,21 +95,36 @@ class VectorIndex(Predictor):
86
95
  )
87
96
  return all_results
88
97
 
98
+ def get_by_idx(self, idx: int) -> np.ndarray:
99
+ return self.vectors[idx]
100
+
101
+ def get_by_idxs(self, idxs: list[int]) -> np.ndarray:
102
+ return self.vectors[idxs]
103
+
104
+ def get_by_label(self, label: Any) -> np.ndarray:
105
+ return self.vectors[self.label_to_idx[label]]
106
+
107
+ def get_by_labels(self, labels: list[Any]) -> np.ndarray:
108
+ idxs = []
109
+ for label in labels:
110
+ idxs.extend(self.label_to_idx[label])
111
+ return self.vectors[idxs]
112
+
89
113
  def aggregate(self, per_label: bool = False) -> "VectorIndex":
90
114
  """
91
115
  Aggregate the vectors into a single centroid per unique label group.
92
116
 
93
117
  Args:
94
118
  per_label: if True, create one centroid per individual label
95
- (splitting label groups apart). If False (default), create
96
- one centroid per unique label group.
119
+ (splitting label groups (tuples) apart). If False (default),
120
+ create one centroid per unique label group.
97
121
  """
98
122
  key_to_indices: dict = {}
99
- for i, label_group in enumerate(self.labels):
100
- if per_label:
101
- keys = [label for label in label_group]
123
+ for i, label in enumerate(self.labels):
124
+ if per_label and isinstance(label, tuple):
125
+ keys = label
102
126
  else:
103
- keys = [tuple(sorted(label_group))]
127
+ keys = [label]
104
128
  for key in keys:
105
129
  if key not in key_to_indices:
106
130
  key_to_indices[key] = []
@@ -119,7 +143,7 @@ class VectorIndex(Predictor):
119
143
  if norm > 0:
120
144
  centroid = centroid / norm
121
145
  centroids[i] = centroid
122
- new_labels.append([key] if per_label else list(key))
146
+ new_labels.append(key)
123
147
 
124
148
  return VectorIndex(vectors=centroids, labels=new_labels)
125
149
 
@@ -161,12 +185,9 @@ class VectorIndex(Predictor):
161
185
  return self.vectors.shape[0]
162
186
 
163
187
  def __repr__(self) -> str:
164
- unique_labels = set()
165
- for group in self.labels:
166
- unique_labels.update(group)
167
188
  return (
168
189
  f"VectorIndex({len(self)} vectors, dim={self.dim}, "
169
- f"{len(unique_labels)} unique labels)"
190
+ f"{len(set(self.labels))} unique labels)"
170
191
  )
171
192
 
172
193
 
@@ -197,11 +218,10 @@ def _collate_pil(batch):
197
218
 
198
219
  def build_vector_index(
199
220
  crops: list[Image.Image | str | CropSpec],
200
- labels: list,
221
+ labels: list[int | str | tuple],
201
222
  feature_extractor,
202
223
  batch_size: int = 128,
203
224
  normalize: bool = True,
204
- multi_label: bool = False,
205
225
  num_workers: int = 0,
206
226
  ) -> VectorIndex:
207
227
  """Build a VectorIndex by extracting features from crops.
@@ -210,14 +230,12 @@ def build_vector_index(
210
230
  crops: images to extract features from. Each element can be a PIL
211
231
  Image (used directly), a file path string (loaded as a whole
212
232
  image), or a CropSpec (loaded and cropped to the specified bbox).
213
- labels: one label per crop. If multi_label is False, each element is a
214
- single label. If multi_label is True, each element should be a list
215
- of labels.
233
+ labels: one label per crop. labels can be any hashable type, includeing
234
+ iterables like tuples.
216
235
  feature_extractor: anything with a .predict(list[Image]) method that
217
236
  returns objects with a .feature attribute (e.g. FeatureExtractionPredictor).
218
237
  batch_size: number of crops to process at once.
219
238
  normalize: whether to L2-normalize the resulting feature vectors.
220
- multi_label: if True, labels are already lists of labels per crop.
221
239
  num_workers: number of DataLoader workers for parallel image loading.
222
240
  """
223
241
  if len(crops) != len(labels):
@@ -225,11 +243,6 @@ def build_vector_index(
225
243
  f"crops length ({len(crops)}) must match labels length ({len(labels)})"
226
244
  )
227
245
 
228
- if multi_label:
229
- index_labels = [list(group) for group in labels]
230
- else:
231
- index_labels = [[label] for label in labels]
232
-
233
246
  loader = DataLoader(
234
247
  _CropDataset(crops),
235
248
  batch_size=batch_size,
@@ -243,4 +256,4 @@ def build_vector_index(
243
256
  all_features.extend([r.feature for r in results])
244
257
 
245
258
  vectors = np.vstack(all_features)
246
- return VectorIndex(vectors=vectors, labels=index_labels, normalize=normalize)
259
+ return VectorIndex(vectors=vectors, labels=labels, normalize=normalize)
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import tempfile
3
+ import warnings
3
4
 
4
5
  import yaml
5
6
  import joblib
@@ -287,7 +288,6 @@ def get_vertex_model(
287
288
  ):
288
289
  vertex_init(project_name, region)
289
290
  models = aiplatform.Model.list(filter=f"display_name={model_name}")
290
-
291
291
  if not models:
292
292
  if raise_exception:
293
293
  raise Exception(
@@ -295,19 +295,26 @@ def get_vertex_model(
295
295
  )
296
296
  else:
297
297
  return None
298
+ if len(models) > 1:
299
+ warnings.warn(
300
+ f"Multiple models found with name '{model_name}'. Using the latest one."
301
+ )
302
+
303
+ latest_model = sorted(models, key=lambda x: x.update_time, reverse=True)[0]
304
+ resource_name = latest_model.resource_name
298
305
 
299
306
  if version is None:
300
- latest_model = sorted(models, key=lambda x: x.update_time, reverse=True)[0]
301
307
  return VertexModel(
302
308
  latest_model, model_name, project_name, region, int(latest_model.version_id)
303
309
  )
304
310
 
305
- for model in models:
306
- if int(model.version_id) == version:
307
- return VertexModel(model, model_name, project_name, region, version)
308
-
309
- if raise_exception:
310
- raise Exception(
311
- f"Model '{model_name}' with version '{version}' not found in registry for project '{project_name}' region '{region}'"
312
- )
313
- return None
311
+ if version is not None:
312
+ try:
313
+ model = aiplatform.Model(model_name=resource_name, version=str(version))
314
+ except Exception:
315
+ if raise_exception:
316
+ raise Exception(
317
+ f"Failed to fetch model '{model_name}' with version '{version}' in registry for project '{project_name}' region '{region}'"
318
+ )
319
+ return None
320
+ return VertexModel(model, model_name, project_name, region, version)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "orient_express"
3
- version = "2.3.1"
3
+ version = "2.4.0"
4
4
  description = "A library to simplify model deployment to Vertex AI"
5
5
  authors = ["Alexey Zankevich <alex.zankevich@shiftsmart.com>", "Ian Myers <ian.myers@shiftsmart.com>"]
6
6
  readme = "README.md"