orient_express 2.3.0__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.0 → orient_express-2.4.0}/PKG-INFO +27 -18
  2. {orient_express-2.3.0 → orient_express-2.4.0}/README.md +25 -17
  3. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/predictors/__init__.py +1 -0
  4. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/predictors/predictor.py +1 -1
  5. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/predictors/vector_index.py +55 -32
  6. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/vertex.py +18 -11
  7. {orient_express-2.3.0 → orient_express-2.4.0}/pyproject.toml +2 -1
  8. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/__init__.py +0 -0
  9. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/deployment.py +0 -0
  10. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/model_wrapper.py +0 -0
  11. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/predictors/classification.py +0 -0
  12. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/predictors/feature_extraction.py +0 -0
  13. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/predictors/instance_segmentation.py +0 -0
  14. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/predictors/multi_label_classification.py +0 -0
  15. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/predictors/object_detection.py +0 -0
  16. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/predictors/semantic_segmentation.py +0 -0
  17. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/sklearn_pipeline.py +0 -0
  18. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/utils/colors.py +0 -0
  19. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/utils/gs.py +0 -0
  20. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/utils/image_processor.py +0 -0
  21. {orient_express-2.3.0 → orient_express-2.4.0}/orient_express/utils/paths.py +0 -0
  22. {orient_express-2.3.0 → 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.0
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
@@ -21,6 +21,7 @@ Requires-Dist: pyyaml (>=6.0,<7.0)
21
21
  Requires-Dist: scikit-learn (>=1.5.0,<2.0.0)
22
22
  Requires-Dist: torch (>=2.5.0)
23
23
  Requires-Dist: torchvision (>=0.20.0)
24
+ Requires-Dist: tqdm (>=4.67.1)
24
25
  Requires-Dist: xgboost (>=2.0.0,<3.0.0)
25
26
  Description-Content-Type: text/markdown
26
27
 
@@ -45,11 +46,13 @@ pip install orient_express
45
46
  ```
46
47
 
47
48
  For local development:
49
+
48
50
  ```bash
49
51
  pip install -e .
50
52
  ```
51
53
 
52
54
  Or with Poetry:
55
+
53
56
  ```bash
54
57
  poetry install
55
58
  ```
@@ -101,8 +104,8 @@ vertex_model.deploy_to_endpoint(
101
104
 
102
105
  # remote prediction API depends on the endpoint container deployed with the model
103
106
  predictions = vertex_model.remote_predict(
104
- [{"image": "https://storage.googleapis.com/ssm-media-uploads/example.jpg"}],
105
- endpoint_name="my-classifier-endpoint"
107
+ "my-classifier-endpoint",
108
+ [{"image": "https://storage.googleapis.com/ssm-media-uploads/example.jpg"}],
106
109
  )
107
110
  ```
108
111
 
@@ -178,19 +181,20 @@ predictions = local_predictor.predict(X_test)
178
181
  ### Platform Support Matrix
179
182
 
180
183
  | Platform | Architecture | ONNX Runtime Package | CUDA Available |
181
- |----------|--------------|---------------------|----------------|
182
- | Linux | x86_64 | onnxruntime-gpu | Yes |
183
- | Linux | aarch64 | onnxruntime | No |
184
- | Windows | x64 (AMD64) | onnxruntime-gpu | Yes |
185
- | Windows | ARM64 | onnxruntime | No |
186
- | macOS | x86_64 | onnxruntime | No |
187
- | 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 |
188
191
 
189
192
  The appropriate package is installed automatically based on your platform.
190
193
 
191
194
  ### Selecting CPU vs CUDA Execution
192
195
 
193
196
  When loading a predictor, use the `device` parameter to specify the execution provider:
197
+
194
198
  ```python
195
199
  from orient_express.predictors import ObjectDetectionPredictor
196
200
 
@@ -202,6 +206,7 @@ predictor = ObjectDetectionPredictor("/path/to/model", classes, device="cuda")
202
206
  ```
203
207
 
204
208
  When using a Vertex AI model:
209
+
205
210
  ```python
206
211
  # CPU inference
207
212
  predictor = model.get_local_predictor(device="cpu")
@@ -539,11 +544,8 @@ index = build_vector_index(
539
544
  num_workers=8, # parallel image loading
540
545
  )
541
546
 
542
- # Aggregate to one centroid per label
543
- centroid_index = index.aggregate()
544
-
545
547
  # Save and load
546
- centroid_index.dump("/path/to/artifact_dir")
548
+ index.dump("/path/to/artifact_dir")
547
549
 
548
550
  from orient_express.predictors import get_predictor
549
551
  loaded_index = get_predictor("/path/to/artifact_dir")
@@ -559,16 +561,23 @@ batch_results = loaded_index.search_batch(query_matrix, k=5)
559
561
 
560
562
  #### Multi-label support
561
563
 
562
- 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:
563
565
 
564
566
  ```python
565
567
  index = VectorIndex(
566
568
  vectors=feature_matrix,
567
- labels=[["sku_101", "sku_102"], ["sku_103"], ["sku_104", "sku_105"]],
569
+ labels=[("sku_101", "sku_102"), ("sku_103",)],
568
570
  )
571
+ ```
572
+
573
+ #### Per-label aggregation
569
574
 
570
- # aggregate() computes one centroid per unique label
571
- 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")]
572
581
  ```
573
582
 
574
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,38 +95,63 @@ class VectorIndex(Predictor):
86
95
  )
87
96
  return all_results
88
97
 
89
- def aggregate(self) -> "VectorIndex":
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
+
113
+ def aggregate(self, per_label: bool = False) -> "VectorIndex":
90
114
  """
91
- Aggregate the vectors into a single centroid per label.
115
+ Aggregate the vectors into a single centroid per unique label group.
116
+
117
+ Args:
118
+ per_label: if True, create one centroid per individual label
119
+ (splitting label groups (tuples) apart). If False (default),
120
+ create one centroid per unique label group.
92
121
  """
93
- label_to_indices: dict = {}
94
- for i, label_group in enumerate(self.labels):
95
- for label in label_group:
96
- if label not in label_to_indices:
97
- label_to_indices[label] = []
98
- label_to_indices[label].append(i)
99
-
100
- unique_labels = sorted(label_to_indices.keys(), key=str)
122
+ key_to_indices: dict = {}
123
+ for i, label in enumerate(self.labels):
124
+ if per_label and isinstance(label, tuple):
125
+ keys = label
126
+ else:
127
+ keys = [label]
128
+ for key in keys:
129
+ if key not in key_to_indices:
130
+ key_to_indices[key] = []
131
+ key_to_indices[key].append(i)
132
+
133
+ sorted_keys = sorted(key_to_indices.keys(), key=str)
101
134
  centroids = np.empty(
102
- (len(unique_labels), self.vectors.shape[1]), dtype=self.vectors.dtype
135
+ (len(sorted_keys), self.vectors.shape[1]), dtype=self.vectors.dtype
103
136
  )
104
137
 
105
- for i, label in enumerate(unique_labels):
106
- indices = label_to_indices[label]
138
+ new_labels = []
139
+ for i, key in enumerate(sorted_keys):
140
+ indices = key_to_indices[key]
107
141
  centroid = self.vectors[indices].mean(axis=0)
108
142
  norm = np.linalg.norm(centroid)
109
143
  if norm > 0:
110
144
  centroid = centroid / norm
111
145
  centroids[i] = centroid
146
+ new_labels.append(key)
112
147
 
113
- new_labels = [[label] for label in unique_labels]
114
148
  return VectorIndex(vectors=centroids, labels=new_labels)
115
149
 
116
150
  def get_serving_container_image_uri(self) -> str:
117
151
  warnings.warn(
118
152
  "VectorIndex does not support serving via a container. Returning incompatible image URI."
119
153
  )
120
- return "us-west1-docker.pkg.dev/shiftsmartinc/orient-express/image-onnx:v2.1.2"
154
+ return "us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:v2.1.2"
121
155
 
122
156
  def get_serving_container_health_route(self, model_name) -> str:
123
157
  return f"/v1/models/{model_name}"
@@ -151,12 +185,9 @@ class VectorIndex(Predictor):
151
185
  return self.vectors.shape[0]
152
186
 
153
187
  def __repr__(self) -> str:
154
- unique_labels = set()
155
- for group in self.labels:
156
- unique_labels.update(group)
157
188
  return (
158
189
  f"VectorIndex({len(self)} vectors, dim={self.dim}, "
159
- f"{len(unique_labels)} unique labels)"
190
+ f"{len(set(self.labels))} unique labels)"
160
191
  )
161
192
 
162
193
 
@@ -187,11 +218,10 @@ def _collate_pil(batch):
187
218
 
188
219
  def build_vector_index(
189
220
  crops: list[Image.Image | str | CropSpec],
190
- labels: list,
221
+ labels: list[int | str | tuple],
191
222
  feature_extractor,
192
223
  batch_size: int = 128,
193
224
  normalize: bool = True,
194
- multi_label: bool = False,
195
225
  num_workers: int = 0,
196
226
  ) -> VectorIndex:
197
227
  """Build a VectorIndex by extracting features from crops.
@@ -200,14 +230,12 @@ def build_vector_index(
200
230
  crops: images to extract features from. Each element can be a PIL
201
231
  Image (used directly), a file path string (loaded as a whole
202
232
  image), or a CropSpec (loaded and cropped to the specified bbox).
203
- labels: one label per crop. If multi_label is False, each element is a
204
- single label. If multi_label is True, each element should be a list
205
- of labels.
233
+ labels: one label per crop. labels can be any hashable type, includeing
234
+ iterables like tuples.
206
235
  feature_extractor: anything with a .predict(list[Image]) method that
207
236
  returns objects with a .feature attribute (e.g. FeatureExtractionPredictor).
208
237
  batch_size: number of crops to process at once.
209
238
  normalize: whether to L2-normalize the resulting feature vectors.
210
- multi_label: if True, labels are already lists of labels per crop.
211
239
  num_workers: number of DataLoader workers for parallel image loading.
212
240
  """
213
241
  if len(crops) != len(labels):
@@ -215,11 +243,6 @@ def build_vector_index(
215
243
  f"crops length ({len(crops)}) must match labels length ({len(labels)})"
216
244
  )
217
245
 
218
- if multi_label:
219
- index_labels = [list(group) for group in labels]
220
- else:
221
- index_labels = [[label] for label in labels]
222
-
223
246
  loader = DataLoader(
224
247
  _CropDataset(crops),
225
248
  batch_size=batch_size,
@@ -233,4 +256,4 @@ def build_vector_index(
233
256
  all_features.extend([r.feature for r in results])
234
257
 
235
258
  vectors = np.vstack(all_features)
236
- 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.0"
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"
@@ -18,6 +18,7 @@ Pillow = ">=10.0.1"
18
18
  gcsfs = "^2024.10.0"
19
19
  scikit-learn = "^1.5.0"
20
20
  xgboost = "^2.0.0"
21
+ tqdm = ">=4.67.1"
21
22
  onnxruntime-gpu = {version = "^1.20", markers = "sys_platform == 'linux' and platform_machine == 'x86_64' or sys_platform == 'win32' and platform_machine == 'AMD64'"}
22
23
  onnxruntime = {version = "^1.20", markers = "sys_platform == 'darwin' or platform_machine == 'aarch64' or platform_machine == 'arm64' or sys_platform == 'win32' and platform_machine == 'ARM64'"}
23
24