Skip to content

Commit 70b6455

Browse files
committed
add mediapipe model
1 parent cc78855 commit 70b6455

File tree

6 files changed

+193
-0
lines changed

6 files changed

+193
-0
lines changed

posemodel/.nboxignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__pycache__/
2+
.git/
3+
.vscode/
4+
venv/

posemodel/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Mediapipe Model
2+
3+
In this example we are going to deploy a (MediaPipe)[https://mediapipe.dev/] model for landmark detection. There are many ways to call a deployed a CV model:
4+
- directly transfering uint8 array: as you would think this creates the heaviest message, this is the worst. Do it just to realise how bad it is.
5+
- transferring the bytes of the image: this is an industry standard approach where the client would send in a base64 encoded image bytes and the server would reconstruct the image, this is a good case when your server is in an unsafe environment
6+
- sending in a URL and server would fetch it: this is good when your server is in a safe environment and you know what are the actual contents of the URL
7+
8+
**Note on opencv**: `mediapipe` has a dependency on `opencv` and installing `opencv` is a bit tricky since it depends directly on the system packages. So we the trick the system by installing it right from inside our script even before `mediapipe` is imported, this may seem like a hack but it is **99% solution that works 99% of times**.
9+
10+
## Serve
11+
12+
The class is defined in `model.py` file and to serve this model run:
13+
```
14+
nbx serve upload model:MediaPipeModel 'mediapipe_model'
15+
```
16+
17+
The way `nbox.Operator` works is that it would take all the functions that you have in a class and create an endpoint against it, in this case:
18+
19+
- `predict` would be served at `method_predict_rest/`, this takes in a raw array and returns predictions
20+
- `predict_b64` would be served at `method_predict_b64_rest/`, this takes in a base64 encoded image
21+
- `predict_url` would be served at `method_predict_url_rest/`, this takes in a URL
22+
23+
The developer is free from writing API endpoints, managing the complexity of on-wire protocols, they simply write functions that can take in any input (for REST it needs to be JSON serialisable).
24+
25+
## Use
26+
27+
The model is now deployed on an API endpoint that looks like this: `https://api.nimblebox.ai/cdlmonrl/`, you can go to the Deploy → 'mediapipe_model' → Settings and get your access key, it would look like this: `nbxdeploy_AZqcVWuVm0pC4k567EaUjOCOulZiQ3YdLEQJNnrR`. The file `predict.py` contains more detailed tests for the API endpoint. Here's from my run:
28+
29+
```
30+
Time taken for array (avg. 10 calls): 9.3824s
31+
Time taken for b64 (avg. 20 calls): 1.2814s
32+
Time taken for url (avg. 50 calls): 0.4145s
33+
```
34+
35+
## Advanced
36+
37+
The `nbox.Operator` is designed to wrap any arbitrary python class or function to become part of a distributed compute fabric. When you have deployed a model on NBX-Deploy you can connect directly via an `Operator` with `.from_serving` classmethod like:
38+
39+
```
40+
mediapipe = Operator.from_serving("https://api.nimblebox.ai/cdlmonrl/", "<token>")
41+
out = mediapipe.predict_url(url)
42+
```
43+
44+
To test it run file:
45+
```
46+
python3 advanced.py
47+
```

posemodel/advanced.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from nbox import Operator
2+
url = "https://i0.wp.com/post.healthline.com/wp-content/uploads/2020/01/Runner-training-on-running-track-1296x728-header-1296x728.jpg?w=1155&h=1528"
3+
4+
mediapipe = Operator.from_serving("https://api.nimblebox.ai/cdlmonrl/", "<token>")
5+
out = mediapipe.predict_url(url)
6+
print(out)

posemodel/model.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import subprocess
2+
from typing import List
3+
subprocess.call(['/model/venv/bin/python3', '-m', 'pip', 'install', 'opencv-python-headless'])
4+
5+
import requests
6+
from io import BytesIO
7+
import itertools
8+
import numpy as np
9+
import mediapipe as mp
10+
11+
import base64
12+
from PIL import Image
13+
14+
from nbox import operator
15+
16+
@operator()
17+
class MediaPipeModel():
18+
def __init__(self):
19+
mp_pose = mp.solutions.pose
20+
self.pose = mp_pose.Pose(
21+
static_image_mode=True, model_complexity=2, min_detection_confidence=0.8
22+
)
23+
24+
def predict(self, image_array):
25+
mp_pose = mp.solutions.pose
26+
landmarks = [
27+
"LEFT_ANKLE","LEFT_EAR","LEFT_ELBOW","LEFT_EYE","LEFT_EYE_INNER","LEFT_EYE_OUTER","LEFT_FOOT_INDEX",
28+
"LEFT_HEEL","LEFT_HIP","LEFT_INDEX","LEFT_KNEE","LEFT_PINKY","LEFT_SHOULDER","LEFT_THUMB","LEFT_WRIST",
29+
"MOUTH_LEFT","MOUTH_RIGHT","NOSE","RIGHT_ANKLE","RIGHT_EAR","RIGHT_ELBOW","RIGHT_EYE","RIGHT_EYE_INNER",
30+
"RIGHT_EYE_OUTER","RIGHT_FOOT_INDEX","RIGHT_HEEL","RIGHT_HIP","RIGHT_INDEX","RIGHT_KNEE","RIGHT_PINKY",
31+
"RIGHT_SHOULDER","RIGHT_THUMB","RIGHT_WRIST",
32+
]
33+
coordinates = ["x", "y", "z", "visibility"]
34+
35+
data = {}
36+
image = np.array(image_array).astype(np.uint8)
37+
image_height, image_width, _ = image.shape
38+
data["image_width"] = image_width
39+
data["image_height"] = image_height
40+
results = self.pose.process(image)
41+
if results.pose_landmarks:
42+
for l, c in itertools.product(landmarks, coordinates):
43+
data[f"{l}_{c}"] = results.pose_landmarks.landmark[mp_pose.PoseLandmark[l]].__getattribute__(c)
44+
return {"pred": data}
45+
46+
def predict_b64(self, image_b64: str, shape: List[int]):
47+
img = Image.frombytes("RGB", shape, base64.b64decode(image_b64))
48+
image_array = np.array(img, dtype = np.uint8).reshape(*shape, 3)
49+
return self.predict(image_array)
50+
51+
def predict_url(self, url: str):
52+
r = requests.get(url)
53+
img = Image.open(BytesIO(r.content))
54+
return self.predict(np.array(img))

posemodel/predict.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import time
2+
import requests
3+
import numpy as np
4+
from PIL import Image
5+
from base64 import b64encode
6+
from io import BytesIO
7+
8+
r = requests.get(
9+
"https://i0.wp.com/post.healthline.com/wp-content/uploads/2020/01/Runner-training-on-running-track-1296x728-header-1296x728.jpg?w=1155&h=1528"
10+
)
11+
img = Image.open(BytesIO(r.content))
12+
_shape = np.array(img).shape[:2]
13+
14+
times = []
15+
n = 10
16+
print("Testing predict_rest")
17+
for _ in range(n):
18+
st = time.time()
19+
r = requests.post(
20+
"https://api.nimblebox.ai/cdlmonrl//method_predict_rest",
21+
headers = {"NBX-KEY": "<token>"},
22+
json = {
23+
"image_array": np.array(img).tolist()
24+
}
25+
)
26+
r.raise_for_status()
27+
et = time.time()
28+
times.append(et - st)
29+
30+
_mt = np.mean(times)
31+
print(f"Time taken for array (avg. {n} calls): {_mt:0.4f}s")
32+
# print(r.json())
33+
34+
35+
times = []
36+
n = 20
37+
print("Testing predict_b64")
38+
for _ in range(n):
39+
st = time.time()
40+
r = requests.post(
41+
"https://api.nimblebox.ai/cdlmonrl//method_predict_b64_rest",
42+
headers = {"NBX-KEY": "<token>"},
43+
json = {
44+
"image_b64": b64encode(img.tobytes()).decode("utf-8"),
45+
"shape": _shape
46+
}
47+
)
48+
r.raise_for_status()
49+
et = time.time()
50+
times.append(et - st)
51+
52+
_mt = np.mean(times)
53+
print(f"Time taken for b64 (avg. {n} calls): {_mt:0.4f}s")
54+
# print(r.json())
55+
56+
times = []
57+
n = 50
58+
print("Testing predict_url")
59+
for _ in range(n):
60+
st = time.time()
61+
r = requests.post(
62+
"https://api.nimblebox.ai/cdlmonrl//method_predict_url_rest",
63+
headers = {"NBX-KEY": "<token>"},
64+
json = {
65+
"url": "https://i0.wp.com/post.healthline.com/wp-content/uploads/2020/01/Runner-training-on-running-track-1296x728-header-1296x728.jpg?w=1155&h=1528"
66+
}
67+
)
68+
r.raise_for_status()
69+
et = time.time()
70+
times.append(et - st)
71+
72+
_mt = np.mean(times)
73+
print(f"Time taken for url (avg. {n} calls): {_mt:0.4f}s")

posemodel/requirements.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
nbox[serving]==0.10.6
2+
numpy
3+
mediapipe
4+
5+
# installing opencv-python-headless through requirement can cause one of the following errors:
6+
# AttributeError: partially initialized module 'cv2' has no attribute '_registerMatType' (most likely due to a circular import)
7+
# ImportError: libGL.so.1: cannot open shared object file: No such file or directory
8+
9+
# pillow

0 commit comments

Comments
 (0)