orient_express 2.4.0__tar.gz → 2.4.2__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.4.0 → orient_express-2.4.2}/PKG-INFO +186 -1
  2. {orient_express-2.4.0 → orient_express-2.4.2}/README.md +185 -0
  3. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/predictors/predictor.py +1 -1
  4. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/predictors/semantic_segmentation.py +15 -6
  5. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/utils/image_processor.py +39 -1
  6. {orient_express-2.4.0 → orient_express-2.4.2}/pyproject.toml +4 -1
  7. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/__init__.py +0 -0
  8. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/deployment.py +0 -0
  9. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/model_wrapper.py +0 -0
  10. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/predictors/__init__.py +0 -0
  11. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/predictors/classification.py +0 -0
  12. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/predictors/feature_extraction.py +0 -0
  13. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/predictors/instance_segmentation.py +0 -0
  14. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/predictors/multi_label_classification.py +0 -0
  15. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/predictors/object_detection.py +0 -0
  16. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/predictors/vector_index.py +0 -0
  17. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/sklearn_pipeline.py +0 -0
  18. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/utils/colors.py +0 -0
  19. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/utils/gs.py +0 -0
  20. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/utils/paths.py +0 -0
  21. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/utils/retry.py +0 -0
  22. {orient_express-2.4.0 → orient_express-2.4.2}/orient_express/vertex.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orient_express
3
- Version: 2.4.0
3
+ Version: 2.4.2
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
@@ -593,6 +593,191 @@ class SearchResult:
593
593
 
594
594
  ---
595
595
 
596
+ ## Deployed Endpoint APIs
597
+
598
+ When you upload a model with orient-express and deploy it to a Vertex AI endpoint, the actual HTTP API exposed by the endpoint is determined by the serving container image — not by your Python predictor code. Orient-express ships two such images:
599
+
600
+ - `image-onnx` — serves the built-in ONNX predictor types (classification, detection, segmentation).
601
+ - `xgboost-scikit-learn` — serves any joblib-loadable model (sklearn pipelines, xgboost, etc.).
602
+
603
+ This section documents the request/response shape each image's endpoint exposes once deployed.
604
+
605
+ ### How the docker images connect to GCP endpoints
606
+
607
+ The deployment flow is:
608
+
609
+ 1. **Train + export.** You build a predictor locally (e.g. `ClassificationPredictor("model.onnx", classes)`) or train a sklearn/xgboost model.
610
+ 2. **Upload.** `upload_model` / `upload_model_joblib` pushes the artifacts to GCS under `gs://<bucket>/models/<model_name>/<version>/` and registers a Vertex AI Model with `serving_container_image_uri` pointing at one of orient-express's images.
611
+ 3. **Deploy.** `vertex_model.deploy_to_endpoint(...)` (or the Vertex console) attaches the registered model to a Vertex AI Endpoint. Vertex starts the container with `AIP_STORAGE_URI` set to the GCS path from step 2, plus `MODEL_NAME` set to the model's display name.
612
+ 4. **Serve.** The container downloads the artifacts on startup, instantiates the right predictor via metadata, and listens on `/v1/models/<MODEL_NAME>:predict`.
613
+ 5. **Call.** Clients POST to `https://<region>-aiplatform.googleapis.com/v1/projects/<project>/locations/<region>/endpoints/<endpoint_id>:predict` with a Bearer token. Vertex routes the request into the container and returns the JSON response.
614
+
615
+ Every endpoint accepts the same envelope:
616
+
617
+ ```json
618
+ {
619
+ "instances": [...],
620
+ "parameters": {...}
621
+ }
622
+ ```
623
+
624
+ What goes in `instances` / `parameters`, and what comes back in `predictions`, is per-image and per-predictor — covered below.
625
+
626
+ ### ONNX Image Endpoint
627
+
628
+ Image: `us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:<tag>`
629
+
630
+ Common request shape across all ONNX predictor types:
631
+
632
+ ```json
633
+ {
634
+ "instances": [
635
+ {"image": "<http(s) URL | gs:// URI | base64 | data URI>"}
636
+ ],
637
+ "parameters": {
638
+ "confidence": 0.5
639
+ }
640
+ }
641
+ ```
642
+
643
+ Common response envelope:
644
+
645
+ ```json
646
+ {
647
+ "predictions": [
648
+ {"status": "success", ...predictor-specific fields...}
649
+ ]
650
+ }
651
+ ```
652
+
653
+ `status` values: `"success"`, `"failed to download image"`, or `"failed to get debug image"`. Malformed payloads return top-level `{"error": "Failed to decode input"}` instead of `predictions`.
654
+
655
+ The predictor-specific fields differ by model type — covered in the subsections below.
656
+
657
+ #### Classification
658
+
659
+ For models uploaded as `ClassificationPredictor`. `parameters.confidence` is **not** honored (the predictor always returns the top class).
660
+
661
+ Per-image response:
662
+
663
+ ```json
664
+ {
665
+ "status": "success",
666
+ "class": "cat",
667
+ "score": 0.95,
668
+ "class_scores": {"cat": 0.95, "dog": 0.03, "bird": 0.02}
669
+ }
670
+ ```
671
+
672
+ No `debug_image` for classification (nothing meaningful to draw).
673
+
674
+ #### Multi-label classification
675
+
676
+ For models uploaded as `MultiLabelClassificationPredictor`. `parameters.confidence` is the per-class threshold for inclusion in `classes` (default `0.5`).
677
+
678
+ Per-image response:
679
+
680
+ ```json
681
+ {
682
+ "status": "success",
683
+ "predictions": {
684
+ "classes": ["contains_cat", "contains_bird"],
685
+ "class_scores": {"contains_cat": 0.95, "contains_dog": 0.03, "contains_bird": 0.82}
686
+ },
687
+ "debug_image": null
688
+ }
689
+ ```
690
+
691
+ `debug_image` is always `null` for multi-label.
692
+
693
+ #### Object detection
694
+
695
+ For models uploaded as `BoundingBoxPredictor`. `parameters.confidence` filters detections below the threshold (default `0.5`).
696
+
697
+ Per-image response:
698
+
699
+ ```json
700
+ {
701
+ "status": "success",
702
+ "predictions": [
703
+ {"class": "person", "score": 0.92, "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}}
704
+ ],
705
+ "debug_image": "<base64 JPEG with boxes overlaid>"
706
+ }
707
+ ```
708
+
709
+ `bbox` coordinates are in pixels of the original (EXIF-corrected) image. `predictions` is an empty list when nothing clears the confidence threshold.
710
+
711
+ #### Instance segmentation
712
+
713
+ For models uploaded as `InstanceSegmentationPredictor`. `parameters.confidence` filters detections (default `0.5`).
714
+
715
+ Per-image response:
716
+
717
+ ```json
718
+ {
719
+ "status": "success",
720
+ "predictions": [
721
+ {"class": "person", "score": 0.89, "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}}
722
+ ],
723
+ "debug_image": "<base64 JPEG with masks overlaid>"
724
+ }
725
+ ```
726
+
727
+ Per-instance mask arrays are **not** included in the response by default (too large). The annotated mask overlay is baked into `debug_image`.
728
+
729
+ #### Semantic segmentation
730
+
731
+ For models uploaded as `SemanticSegmentationPredictor`. `parameters.confidence` is the per-pixel threshold above which a class is considered "valid" (default `0.5`).
732
+
733
+ Per-image response:
734
+
735
+ ```json
736
+ {
737
+ "status": "success",
738
+ "predictions": {
739
+ "class_mask": "<base64 PNG, uint8, per-pixel class id>",
740
+ "valid_mask": "<base64 PNG, uint8, 0=below threshold, 1=above>"
741
+ },
742
+ "debug_image": "<base64 JPEG with color-coded overlay>"
743
+ }
744
+ ```
745
+
746
+ `class_mask` always paints every pixel with the argmax winner. `valid_mask` tells you which pixels actually cleared the confidence threshold — AND them together client-side to get the "real" segmentation.
747
+
748
+ ### XGBoost / scikit-learn Endpoint
749
+
750
+ Image: `us-west1-docker.pkg.dev/shiftsmart-api/orient-express/xgboost-scikit-learn:<tag>`
751
+
752
+ For models uploaded via `upload_model_joblib` — sklearn pipelines, xgboost models, or anything `joblib.load`-able with a `.predict(DataFrame)` method.
753
+
754
+ Request shape — `instances` is a list of dicts, one row per input:
755
+
756
+ ```json
757
+ {
758
+ "instances": [
759
+ {"pclass": 1, "sex": "female", "age": 29, "fare": 100.0, "embarked": "S"},
760
+ {"pclass": 3, "sex": "male", "age": 35, "fare": 8.05, "embarked": "S"}
761
+ ]
762
+ }
763
+ ```
764
+
765
+ The server constructs `pd.DataFrame(instances)` and calls `model.predict(df)` on it. The columns your pipeline expects must be present in each instance dict.
766
+
767
+ Response shape — one prediction per input row:
768
+
769
+ ```json
770
+ {
771
+ "predictions": [0, 1]
772
+ }
773
+ ```
774
+
775
+ Each element is whatever your `.predict()` returns — a class label for classifiers, a numeric value for regressors, an array for multi-output models.
776
+
777
+ `parameters` is ignored — there's no per-request configuration for this image.
778
+
779
+ ---
780
+
596
781
  ## Color Schemes
597
782
 
598
783
  For predictors that support annotation (`BoundingBoxPredictor`, `InstanceSegmentationPredictor`, `SemanticSegmentationPredictor`), you can set a custom color scheme:
@@ -566,6 +566,191 @@ class SearchResult:
566
566
 
567
567
  ---
568
568
 
569
+ ## Deployed Endpoint APIs
570
+
571
+ When you upload a model with orient-express and deploy it to a Vertex AI endpoint, the actual HTTP API exposed by the endpoint is determined by the serving container image — not by your Python predictor code. Orient-express ships two such images:
572
+
573
+ - `image-onnx` — serves the built-in ONNX predictor types (classification, detection, segmentation).
574
+ - `xgboost-scikit-learn` — serves any joblib-loadable model (sklearn pipelines, xgboost, etc.).
575
+
576
+ This section documents the request/response shape each image's endpoint exposes once deployed.
577
+
578
+ ### How the docker images connect to GCP endpoints
579
+
580
+ The deployment flow is:
581
+
582
+ 1. **Train + export.** You build a predictor locally (e.g. `ClassificationPredictor("model.onnx", classes)`) or train a sklearn/xgboost model.
583
+ 2. **Upload.** `upload_model` / `upload_model_joblib` pushes the artifacts to GCS under `gs://<bucket>/models/<model_name>/<version>/` and registers a Vertex AI Model with `serving_container_image_uri` pointing at one of orient-express's images.
584
+ 3. **Deploy.** `vertex_model.deploy_to_endpoint(...)` (or the Vertex console) attaches the registered model to a Vertex AI Endpoint. Vertex starts the container with `AIP_STORAGE_URI` set to the GCS path from step 2, plus `MODEL_NAME` set to the model's display name.
585
+ 4. **Serve.** The container downloads the artifacts on startup, instantiates the right predictor via metadata, and listens on `/v1/models/<MODEL_NAME>:predict`.
586
+ 5. **Call.** Clients POST to `https://<region>-aiplatform.googleapis.com/v1/projects/<project>/locations/<region>/endpoints/<endpoint_id>:predict` with a Bearer token. Vertex routes the request into the container and returns the JSON response.
587
+
588
+ Every endpoint accepts the same envelope:
589
+
590
+ ```json
591
+ {
592
+ "instances": [...],
593
+ "parameters": {...}
594
+ }
595
+ ```
596
+
597
+ What goes in `instances` / `parameters`, and what comes back in `predictions`, is per-image and per-predictor — covered below.
598
+
599
+ ### ONNX Image Endpoint
600
+
601
+ Image: `us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:<tag>`
602
+
603
+ Common request shape across all ONNX predictor types:
604
+
605
+ ```json
606
+ {
607
+ "instances": [
608
+ {"image": "<http(s) URL | gs:// URI | base64 | data URI>"}
609
+ ],
610
+ "parameters": {
611
+ "confidence": 0.5
612
+ }
613
+ }
614
+ ```
615
+
616
+ Common response envelope:
617
+
618
+ ```json
619
+ {
620
+ "predictions": [
621
+ {"status": "success", ...predictor-specific fields...}
622
+ ]
623
+ }
624
+ ```
625
+
626
+ `status` values: `"success"`, `"failed to download image"`, or `"failed to get debug image"`. Malformed payloads return top-level `{"error": "Failed to decode input"}` instead of `predictions`.
627
+
628
+ The predictor-specific fields differ by model type — covered in the subsections below.
629
+
630
+ #### Classification
631
+
632
+ For models uploaded as `ClassificationPredictor`. `parameters.confidence` is **not** honored (the predictor always returns the top class).
633
+
634
+ Per-image response:
635
+
636
+ ```json
637
+ {
638
+ "status": "success",
639
+ "class": "cat",
640
+ "score": 0.95,
641
+ "class_scores": {"cat": 0.95, "dog": 0.03, "bird": 0.02}
642
+ }
643
+ ```
644
+
645
+ No `debug_image` for classification (nothing meaningful to draw).
646
+
647
+ #### Multi-label classification
648
+
649
+ For models uploaded as `MultiLabelClassificationPredictor`. `parameters.confidence` is the per-class threshold for inclusion in `classes` (default `0.5`).
650
+
651
+ Per-image response:
652
+
653
+ ```json
654
+ {
655
+ "status": "success",
656
+ "predictions": {
657
+ "classes": ["contains_cat", "contains_bird"],
658
+ "class_scores": {"contains_cat": 0.95, "contains_dog": 0.03, "contains_bird": 0.82}
659
+ },
660
+ "debug_image": null
661
+ }
662
+ ```
663
+
664
+ `debug_image` is always `null` for multi-label.
665
+
666
+ #### Object detection
667
+
668
+ For models uploaded as `BoundingBoxPredictor`. `parameters.confidence` filters detections below the threshold (default `0.5`).
669
+
670
+ Per-image response:
671
+
672
+ ```json
673
+ {
674
+ "status": "success",
675
+ "predictions": [
676
+ {"class": "person", "score": 0.92, "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}}
677
+ ],
678
+ "debug_image": "<base64 JPEG with boxes overlaid>"
679
+ }
680
+ ```
681
+
682
+ `bbox` coordinates are in pixels of the original (EXIF-corrected) image. `predictions` is an empty list when nothing clears the confidence threshold.
683
+
684
+ #### Instance segmentation
685
+
686
+ For models uploaded as `InstanceSegmentationPredictor`. `parameters.confidence` filters detections (default `0.5`).
687
+
688
+ Per-image response:
689
+
690
+ ```json
691
+ {
692
+ "status": "success",
693
+ "predictions": [
694
+ {"class": "person", "score": 0.89, "bbox": {"x1": 100.5, "y1": 50.2, "x2": 300.8, "y2": 400.1}}
695
+ ],
696
+ "debug_image": "<base64 JPEG with masks overlaid>"
697
+ }
698
+ ```
699
+
700
+ Per-instance mask arrays are **not** included in the response by default (too large). The annotated mask overlay is baked into `debug_image`.
701
+
702
+ #### Semantic segmentation
703
+
704
+ For models uploaded as `SemanticSegmentationPredictor`. `parameters.confidence` is the per-pixel threshold above which a class is considered "valid" (default `0.5`).
705
+
706
+ Per-image response:
707
+
708
+ ```json
709
+ {
710
+ "status": "success",
711
+ "predictions": {
712
+ "class_mask": "<base64 PNG, uint8, per-pixel class id>",
713
+ "valid_mask": "<base64 PNG, uint8, 0=below threshold, 1=above>"
714
+ },
715
+ "debug_image": "<base64 JPEG with color-coded overlay>"
716
+ }
717
+ ```
718
+
719
+ `class_mask` always paints every pixel with the argmax winner. `valid_mask` tells you which pixels actually cleared the confidence threshold — AND them together client-side to get the "real" segmentation.
720
+
721
+ ### XGBoost / scikit-learn Endpoint
722
+
723
+ Image: `us-west1-docker.pkg.dev/shiftsmart-api/orient-express/xgboost-scikit-learn:<tag>`
724
+
725
+ For models uploaded via `upload_model_joblib` — sklearn pipelines, xgboost models, or anything `joblib.load`-able with a `.predict(DataFrame)` method.
726
+
727
+ Request shape — `instances` is a list of dicts, one row per input:
728
+
729
+ ```json
730
+ {
731
+ "instances": [
732
+ {"pclass": 1, "sex": "female", "age": 29, "fare": 100.0, "embarked": "S"},
733
+ {"pclass": 3, "sex": "male", "age": 35, "fare": 8.05, "embarked": "S"}
734
+ ]
735
+ }
736
+ ```
737
+
738
+ The server constructs `pd.DataFrame(instances)` and calls `model.predict(df)` on it. The columns your pipeline expects must be present in each instance dict.
739
+
740
+ Response shape — one prediction per input row:
741
+
742
+ ```json
743
+ {
744
+ "predictions": [0, 1]
745
+ }
746
+ ```
747
+
748
+ Each element is whatever your `.predict()` returns — a class label for classifiers, a numeric value for regressors, an array for multi-output models.
749
+
750
+ `parameters` is ignored — there's no per-request configuration for this image.
751
+
752
+ ---
753
+
569
754
  ## Color Schemes
570
755
 
571
756
  For predictors that support annotation (`BoundingBoxPredictor`, `InstanceSegmentationPredictor`, `SemanticSegmentationPredictor`), you can set a custom color scheme:
@@ -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.4.0"
49
+ return "us-west1-docker.pkg.dev/shiftsmart-api/orient-express/image-onnx:v2.4.2"
50
50
 
51
51
  def get_serving_container_health_route(self, model_name):
52
52
  return f"/v1/models/{model_name}"
@@ -7,17 +7,19 @@ import torch
7
7
  import torch.nn.functional as F
8
8
 
9
9
  from .predictor import OnnxSessionWrapper, ImagePredictor
10
- from ..utils.image_processor import pil_to_opencv, opencv_to_pil
10
+ from ..utils.image_processor import mask_to_base64, pil_to_opencv, opencv_to_pil
11
11
 
12
12
 
13
13
  @dataclass
14
14
  class SemanticSegmentationPrediction:
15
15
  class_mask: np.ndarray
16
+ valid_mask: np.ndarray
16
17
  conf_masks: np.ndarray
17
18
 
18
19
  def to_dict(self, include_conf_masks: bool = False):
19
20
  dict_repr = {
20
- "class_mask": self.class_mask,
21
+ "class_mask": mask_to_base64(self.class_mask.astype(np.uint8)),
22
+ "valid_mask": mask_to_base64(self.valid_mask.astype(np.uint8)),
21
23
  }
22
24
  if include_conf_masks:
23
25
  dict_repr["conf_masks"] = self.conf_masks.tolist()
@@ -60,31 +62,38 @@ class SemanticSegmentationPredictor(ImagePredictor):
60
62
  backend_model = OnnxSemanticSegmentation
61
63
 
62
64
  def predict(
63
- self, images: list[Image.Image]
65
+ self, images: list[Image.Image], confidence: float = 0.5
64
66
  ) -> list[SemanticSegmentationPrediction]:
65
67
  if not images:
66
68
  return []
67
69
  raw_outputs = self.model(images)
68
70
  outputs = []
69
71
  for masks in raw_outputs:
70
- class_mask = np.argmax(masks, axis=0)
72
+ class_mask = np.argmax(masks, axis=0).astype(np.uint8)
73
+ valid_mask = np.max(masks, axis=0) >= confidence
71
74
  outputs.append(
72
75
  SemanticSegmentationPrediction(
73
76
  class_mask=class_mask,
77
+ valid_mask=valid_mask,
74
78
  conf_masks=masks,
75
79
  )
76
80
  )
77
81
  return outputs
78
82
 
79
83
  def get_annotated_image(
80
- self, image: Image.Image, mask: np.array, mask_opacity: float = 0.3
84
+ self,
85
+ image: Image.Image,
86
+ prediction: SemanticSegmentationPrediction,
87
+ mask_opacity: float = 0.3,
81
88
  ) -> Image.Image:
82
89
  opencv_image = pil_to_opencv(image)
83
90
  mask_overlay = opencv_image.copy()
84
91
 
85
92
  for class_id, class_name in self.classes.items():
86
93
  fill_color = self.color_scheme.get(class_name, (120, 120, 120))
87
- mask_overlay[mask == class_id] = fill_color[:3]
94
+ mask_overlay[
95
+ (prediction.class_mask == class_id) & prediction.valid_mask
96
+ ] = fill_color[:3]
88
97
 
89
98
  annotated_image = cv2.addWeighted(
90
99
  mask_overlay, mask_opacity, opencv_image, 1 - mask_opacity, 0
@@ -1,6 +1,9 @@
1
1
  import base64
2
+ import ipaddress
2
3
  import logging
4
+ import socket
3
5
  from io import BytesIO
6
+ from urllib.parse import urlparse
4
7
 
5
8
  import cv2
6
9
  import numpy as np
@@ -9,6 +12,34 @@ import requests
9
12
 
10
13
  from .gs import get_gcs_from_http_url, read_file_bytes
11
14
 
15
+ DOWNLOAD_TIMEOUT_SECONDS = 30
16
+
17
+
18
+ class UnsafeUrlError(ValueError):
19
+ pass
20
+
21
+
22
+ def validate_url(http_url):
23
+ # Reject URLs that could be used for SSRF: the request runs server-side
24
+ # with the service account's network access, so only allow http(s) URLs
25
+ # that resolve to public IPs (blocks the GCE metadata server, localhost,
26
+ # and private/internal ranges).
27
+ parsed = urlparse(http_url)
28
+ if parsed.scheme not in ("http", "https"):
29
+ raise UnsafeUrlError(f"unsupported URL scheme: {parsed.scheme}")
30
+ if not parsed.hostname:
31
+ raise UnsafeUrlError("URL has no hostname")
32
+ try:
33
+ addr_infos = socket.getaddrinfo(parsed.hostname, None)
34
+ except socket.gaierror as e:
35
+ raise UnsafeUrlError(f"could not resolve host: {parsed.hostname}") from e
36
+ for addr_info in addr_infos:
37
+ ip = ipaddress.ip_address(addr_info[4][0])
38
+ if not ip.is_global:
39
+ raise UnsafeUrlError(
40
+ f"URL host {parsed.hostname} resolves to non-public address {ip}"
41
+ )
42
+
12
43
 
13
44
  def read_image_from_url(http_url, http_as_gcs=False) -> Image.Image:
14
45
  # Extract GSC URI from http link and download the file directly.
@@ -19,7 +50,8 @@ def read_image_from_url(http_url, http_as_gcs=False) -> Image.Image:
19
50
  if gs_uri:
20
51
  return read_image_from_gs(gs_uri)
21
52
 
22
- response = requests.get(http_url)
53
+ validate_url(http_url)
54
+ response = requests.get(http_url, timeout=DOWNLOAD_TIMEOUT_SECONDS)
23
55
  response.raise_for_status()
24
56
  image = Image.open(BytesIO(response.content))
25
57
  return image
@@ -74,6 +106,12 @@ def image_to_base64(image):
74
106
  return base64.b64encode(bytes_content).decode("utf-8")
75
107
 
76
108
 
109
+ def mask_to_base64(mask: np.ndarray) -> str:
110
+ buffered = BytesIO()
111
+ Image.fromarray(mask).save(buffered, format="PNG")
112
+ return base64.b64encode(buffered.getvalue()).decode("utf-8")
113
+
114
+
77
115
  def image_to_bytes(image):
78
116
  buffered = BytesIO()
79
117
  if image.mode == "RGBA":
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "orient_express"
3
- version = "2.4.0"
3
+ version = "2.4.2"
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"
@@ -25,6 +25,9 @@ onnxruntime = {version = "^1.20", markers = "sys_platform == 'darwin' or platfor
25
25
  [tool.poetry.group.dev.dependencies]
26
26
  black = "24.10.0"
27
27
  pytest = "8.3.3"
28
+ ipykernel = "^7.2.0"
29
+
30
+ [tool.poetry.group.server.dependencies]
28
31
  kserve = "^0.16.0"
29
32
  python-json-logger = "^4.0.0"
30
33