Just recently, I had written a simple tutorial on FastAPI, which was about simplifying and understanding how APIs work, and creating a simple API using the framework.
That post got quite a good response, but the most asked question was how to deploy the FastAPI API on ec2 and how to use images data rather than simple strings, integers, and floats as input to the API.
I scoured the net for this, but all I could find was some undercooked documentation and a lot of different ways people were taking to deploy using NGINX or ECS. None of those seemed particularly great or complete to me.
So, I tried to do this myself using some help from FastAPI documentation . In this post, we will look at predominantly four things:
Setting Up an Amazon Instance
Creating a FastAPI API for Object Detection
Deploying FastAPI using Docker
An End to End App with UI
So, without further ado, let’s get started.
You can skip any part you feel you are versed with though I would expect you to go through the whole post, long as it may be, as there’s a lot of interconnection between concepts.
Before we start with using the Amazon ec2 instance, we need to set one up. You might need to sign up with your email ID and set up the payment information on the AWS website . Works just like a single sign-on. From here, I will assume that you have an AWS account and so I am going to explain the next essential parts so you can follow through.
Go to AWS Management Console using https://us-west-2.console.aws.amazon.com/console .
On the AWS Management Console, you can select “Launch a Virtual Machine.” Here we are trying to set up the machine where we will deploy our FastAPI API.
In the first step, you need to choose the AMI template for the machine. I am selecting the 18.04 Ubuntu Server since Ubuntu.
Public DNS (IPv4): ec2-18-237-28-174.us-west-2.compute.amazonaws.com
IPv4 Public IP: 18.237.28.174
# run fist command if fastapi.txt gets downloaded.
# mv fastapi.txt fastapi.pem
chmod 400 fastapi.pem
ssh -i "fastapi.pem" ubuntu@<Your Public DNS(IPv4) Address>
Now we have got our Amazon instance up and running. We can move on here to the real part of the post.
Before we deploy an API, we need to have an API with us, right? In one of my last posts, I had written a simple tutorial to understand FastAPI and API basics. Do read the post if you want to understand FastAPI basics.
So, here I will try to create an Image detection API. As for how to pass the Image data to the API? The idea is — What is an image but a string? An image is just made up of bytes, and we can encode these bytes as a string. We will use the base64 string representation, which is a popular way to get binary data to ASCII characters. And, we will pass this string representation to give an image to our API.
So, let us first see how we can convert an Image to a String. We read the binary data from an image file using the ‘rb’ flag and turn it into a base64 encoded data representation using the base64.b64encode function. We then use the decode to utf-8 function to get the base encoded data into human-readable characters. Don’t worry if it doesn’t make a lot of sense right now. Just understand that any data is binary, and we can convert binary data to its string representation using a series of steps.
As a simple example, if I have a simple image like below, we can convert it to a string using:
import base64
with open("sample_images/dog_with_ball.jpg", "rb") as image_file:
base64str = base64.b64encode(image_file.read()).decode("utf-8")
Here I have got a string representation of a file named dog_with_ball.png on my laptop.
Great, we now have a string representation of an image. And, we can send this string representation to our FastAPI. But we also need to have a way to read an image back from its string representation. After all, our image detection API using PyTorch and any other package needs to have an image object that they can predict, and those methods don’t work on a string.
So here is a way to create a PIL image back from an image’s base64 string. Mostly we just do the reverse steps in the same order. We encode in ‘utf-8’ using .encode. We then use base64.b64decode to decode to bytes. We use these bytes to create a bytes object using io.BytesIO and use Image.open to open this bytes IO object as a PIL image, which can easily be used as an input to my PyTorch prediction code.*** Again simply, it is just a way to convert base64 image string to an actual image.***
import base64
import io
from PIL import Image
def base64str_to_PILImage(base64str):
base64_img_bytes = base64str.encode('utf-8')
base64bytes = base64.b64decode(base64_img_bytes)
bytesObj = io.BytesIO(base64bytes)
img = Image.open(bytesObj)
return img
So does this function work? Let’s see for ourselves. We can use just the string to get back the image.
And we have our happy dog back again. Looks better than the string.
So, as now we understand that our API can get an image as a string from our user, let’s create an object detection API that makes use of this image as a string and outputs the bounding boxes for the object with the object classes as well.
Here, I will be using a Pytorch pre-trained fasterrcnn_resnet50_fpn detection model from the torchvision.models for object detection, which is trained on the COCO dataset to keep the code simple, but one can use any model. You can look at these posts if you want to train your custom image classification or image detection model using Pytorch.
Below is the full code for the FastAPI. Although it may look long, we already know all the parts. In this code, we essentially do the following steps:
Create our fast API app using the FastAPI() constructor.
Load our model and the classes it was trained on. I got the list of classes from the PyTorch docs .
We also defined a new class Input , which uses a library called pydantic to validate the input data types that we will get from the API end-user. Here the end-user gives the base64str and some score threshold for object detection prediction.
We add a function called base64str_to_PILImage which does just what it is named.
And we write a predict function called get_predictionbase64 which returns a dict of bounding boxes and classes using a base64 string representation of an image and a threshold as an input. We also add @app .put(“/predict”) on top of this function to define our endpoint. If you need to understand put and endpoint refer to my previous post on FastAPI.
from fastapi import FastAPI
from pydantic import BaseModel
import torchvision
from torchvision import transforms
import torch
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from PIL import Image
import numpy as np
import cv2
import io, json
import base64
app = FastAPI()
# load a pre-trained Model and convert it to eval mode.
# This model loads just once when we start the API.
model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=True)
COCO_INSTANCE_CATEGORY_NAMES = [
'__background__', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus',
'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'N/A', 'stop sign',
'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
'elephant', 'bear', 'zebra', 'giraffe', 'N/A', 'backpack', 'umbrella', 'N/A', 'N/A',
'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball',
'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket',
'bottle', 'N/A', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl',
'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'N/A', 'dining table',
'N/A', 'N/A', 'toilet', 'N/A', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'N/A', 'book',
'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'
]
model.eval()
# define the Input class
class Input(BaseModel):
base64str : str
threshold : float
def base64str_to_PILImage(base64str):
base64_img_bytes = base64str.encode('utf-8')
base64bytes = base64.b64decode(base64_img_bytes)
bytesObj = io.BytesIO(base64bytes)
img = Image.open(bytesObj)
return img
@app.put("/predict")
def get_predictionbase64(d:Input):
'''
FastAPI API will take a base 64 image as input and return a json object
'''
# Load the image
img = base64str_to_PILImage(d.base64str)
# Convert image to tensor
transform = transforms.Compose([transforms.ToTensor()])
img = transform(img)
# get prediction on image
pred = model([img])
pred_class = [COCO_INSTANCE_CATEGORY_NAMES[i] for i in list(pred[0]['labels'].numpy())]
pred_boxes = [[(float(i[0]), float(i[1])), (float(i[2]), float(i[3]))] for i in list(pred[0]['boxes'].detach().numpy())]
pred_score = list(pred[0]['scores'].detach().numpy())
pred_t = [pred_score.index(x) for x in pred_score if x > d.threshold][-1]
pred_boxes = pred_boxes[:pred_t+1]
pred_class = pred_class[:pred_t+1]
return {'boxes': pred_boxes,
'classes' : pred_class}
Before we move on to AWS, let us check if the code works on our local machine. We can start the API on our laptop using:
uvicorn fastapiapp:app --reload
The above means that your API is now running on your local server, and the –reload flag indicates that the API gets updated automatically when you change the fastapiapp.py file. This is very helpful while developing and testing, but you should remove this –reload flag when you put the API in production.
You should see something like:
You can now try to access this API and see if it works using the requests module:
import requests,json
payload = json.dumps({
"base64str": base64str,
"threshold": 0.5
})
response = requests.put("[http://127.0.0.1:8000/predict](http://127.0.0.1:8000/predict)",data = payload)
data_dict = response.json()
And so we get our results using the API. This image contains a dog and a sports ball. We also have corner 1 (x1,y1) and corner 2 (x2,y2) coordinates of our bounding boxes.
Although not strictly necessary, we can visualize how the results look in our Jupyter notebook:
from PIL import Image
import numpy as np
import cv2
import matplotlib.pyplot as plt
def PILImage_to_cv2(img):
return np.asarray(img)
def drawboundingbox(img, boxes,pred_cls, rect_th=2, text_size=1, text_th=2):
img = PILImage_to_cv2(img)
class_color_dict = {}
#initialize some random colors for each class for better looking bounding boxes
for cat in pred_cls:
class_color_dict[cat] = [random.randint(0, 255) for _ in range(3)]
for i in range(len(boxes)):
cv2.rectangle(img, (int(boxes[i][0][0]), int(boxes[i][0][1])),
(int(boxes[i][1][0]),int(boxes[i][1][1])),
color=class_color_dict[pred_cls[i]], thickness=rect_th)
cv2.putText(img,pred_cls[i], (int(boxes[i][0][0]), int(boxes[i][0][1])), cv2.FONT_HERSHEY_SIMPLEX, text_size, class_color_dict[pred_cls[i]],thickness=text_th) # Write the prediction class
plt.figure(figsize=(20,30))
plt.imshow(img)
plt.xticks([])
plt.yticks([])
plt.show()
img = Image.open("sample_images/dog_with_ball.jpg")
drawboundingbox(img, data_dict['boxes'], data_dict['classes'])
Here is the output:
Here you will note that I got the image from the local file system, and that sort of can be considered as cheating as we don’t want to save every file that the user sends to us through a web UI. We should have been able to use the same base64string object that we also had to create this image. Right?
Not to worry, we could do that too. Remember our base64str_to_PILImage function? We could have used that also.
img = base64str_to_PILImage(base64str)
drawboundingbox(img, data_dict['boxes'], data_dict['classes'])
That looks great. We have our working FastAPI, and we also have our amazon instance. We can now move on to Deployment.
Till now, we have created an AWS instance and, we have also created a FastAPI that takes as input a base64 string representation of an image and returns bounding boxes and the associated class. But all the FastAPI code still resides in our local machine. How do we put it on the ec2 server? And run predictions on the cloud.
We will deploy our app using docker, as is suggested by the fastAPI creator himself. I will try to explain how docker works as we go. The below part may look daunting but it just is a series of commands and steps. So stay with me.
We can start by installing docker using:
sudo apt-get update
sudo apt install docker.io
We then start the docker service using:
sudo service docker start
└── dockerfastapi
├── Dockerfile
├── app
│ └── main.py
└── requirements.txt
Here dockerfastapi is our project’s main folder. And here are the different files in this folder:
i. requirements.txt: Docker needs a file, which tells it which all libraries are required for our app to run. Here I have listed all the libraries I used in my Fastapi API.
numpy
opencv-python
matplotlib
torchvision
torch
fastapi
pydantic
ii. Dockerfile: The second file is Dockerfile.
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
COPY ./app /app
COPY requirements.txt .
RUN pip --no-cache-dir install -r requirements.txt
How Docker works?: You can skip this section, but it will help to get some understanding of how docker works.
The dockerfile can be thought of something like a sh file,which contains commands to create a docker image that can be run in a container. One can think of a docker image as an environment where everything like Python and Python libraries is installed. A container is a unit which is just an isolated box in our system that uses a dockerimage. The advantage of using docker is that we can create multiple docker images and use them in multiple containers. For example, one image might contain python36, and another can contain python37. And we can spawn multiple containers in a single Linux server.
Our Dockerfile contains a few things:
FROM command: Here the first line FROM specifies that we start with tiangolo’s (FastAPI creator) Docker image. As per his site: “This image has an “auto-tuning” mechanism included so that you can just add your code and get that same high performance automatically. And without making sacrifices”. What we are doing is just starting from an image that installs python3.7 for us along with some added configurations for uvicorn and gunicorn ASGI servers and a start.sh file for ASGI servers automatically. For adventurous souls, particularly commandset 1 and commandset2 get executed through a sort of a daisy-chaining of commands.
COPY command: We can think of a docker image also as a folder that contains files and such. Here we copy our app folder and the requirements.txt file, which we created earlier to our docker image.
RUN Command: We run pip install command to install all our python dependencies using the requirements.txt file that is now on the docker image.
iii. main.py: This file contains the fastapiapp.py code we created earlier. Remember to keep the name of the file main.py only.
We have got all our files in the required structure, but we haven’t yet used any docker command. We will first need to build an image containing all dependencies using Dockerfile.
We can do this simply by:
sudo docker build -t myimage .
This downloads, copies and installs some files and libraries from tiangolo’s image and creates an image called myimage. This myimage has python37 and some python packages as specified by requirements.txt file.
We will then just need to start a container that runs this image. We can do this using:
sudo docker run -d --name mycontainer -p 80:80 myimage
This will create a container named mycontainer which runs our docker image myimage. The part 80:80 connects our docker container port 80 to our Linux machine port 80.
And actually that’s it. At this point, you should be able to open the below URL in your browser.
# <IPV4 public IP>/docs
URL: 18.237.28.174/docs
And we can check our app programmatically using:
payload = json.dumps({
"base64str": base64str,
"threshold": 0.5
})
response = requests.put("[http://18.237.28.174/predict](http://18.237.28.174/predict)",data = payload)
data_dict = response.json()
print(data_dict)
Yup, finally our API is deployed.
All the above was good and will just work out of the box if you follow the exact instructions, but the real world doesn’t work like that. You will surely get some errors along the way and would need to debug your code. So to help you with that, some docker commands may come handy:
sudo docker logs -f mycontainer
sudo service docker stop
sudo service docker start
sudo docker container ls
sudo docker image ls
# the prune command removes the unused containers and images
sudo docker system prune
# delete a particular container
sudo docker rm mycontainer
# remove myimage
sudo docker image rm myimage
# remove all images
sudo docker image prune — all
curl localhost
sudo docker run -d -p 80:80 -v $(pwd):/app myimage /start-reload.sh
If this doesn’t seem sufficient, adding here a docker cheat sheet containing useful docker commands:
We are done here with our API creation, but we can also create a UI based app using Streamlit using our FastAPI API. This is not how you will do it in a production setting (where you might have developers making apps using react, node.js or javascript)but is mostly here to check the end-to-end flow of how to use an image API. I will host this barebones Streamlit app on local rather than the ec2 server, and it will get the bounding box info and classes from the FastAPI API hosted on ec2.
If you need to learn more about how streamlit works, you can check out this post . Also, if you would want to deploy this streamlit app also to ec2, here is a tutorial again.
Here is the flow of the whole app with UI and FastAPI API on ec2:
Project Architecture
The most important problems we need to solve in our streamlit app are:
A. Using File uploader: We can use the file uploader using:
bytesObj = st.file_uploader(“Choose an image file”)
The next problem is, what is this bytesObj we get from the streamlit file uploader? In streamlit, we will get a bytesIO object from the file_uploader and we will need to convert it to base64str for our FastAPI app input. This can be done using:
def bytesioObj_to_base64str(bytesObj):
return base64.b64encode(bytesObj.read()).decode("utf-8")
base64str = bytesioObj_to_base64str(bytesObj)
B. Using URL: We can also get an image URL from the user using text_input.
url = st.text_input(‘Enter URL’)
We can then get image from URL in base64 string format using the requests module and base64 encode and utf-8 decode:
def ImgURL_to_base64str(url):
return base64.b64encode(requests.get(url).content).decode("utf-8")
base64str = ImgURL_to_base64str(url)
And here is the complete code of our Streamlit app. You have seen most of the code in this post already.
import streamlit as st
import base64
import io
import requests,json
from PIL import Image
import cv2
import numpy as np
import matplotlib.pyplot as plt
import requests
import random
# use file uploader object to recieve image
# Remember that this bytes object can be used only once
def bytesioObj_to_base64str(bytesObj):
return base64.b64encode(bytesObj.read()).decode("utf-8")
# Image conversion functions
def base64str_to_PILImage(base64str):
base64_img_bytes = base64str.encode('utf-8')
base64bytes = base64.b64decode(base64_img_bytes)
bytesObj = io.BytesIO(base64bytes)
img = Image.open(bytesObj)
return img
def PILImage_to_cv2(img):
return np.asarray(img)
def ImgURL_to_base64str(url):
return base64.b64encode(requests.get(url).content).decode("utf-8")
def drawboundingbox(img, boxes,pred_cls, rect_th=2, text_size=1, text_th=2):
img = PILImage_to_cv2(img)
class_color_dict = {}
#initialize some random colors for each class for better looking bounding boxes
for cat in pred_cls:
class_color_dict[cat] = [random.randint(0, 255) for _ in range(3)]
for i in range(len(boxes)):
cv2.rectangle(img, (int(boxes[i][0][0]), int(boxes[i][0][1])),
(int(boxes[i][1][0]),int(boxes[i][1][1])),
color=class_color_dict[pred_cls[i]], thickness=rect_th)
cv2.putText(img,pred_cls[i], (int(boxes[i][0][0]), int(boxes[i][0][1])), cv2.FONT_HERSHEY_SIMPLEX, text_size, class_color_dict[pred_cls[i]],thickness=text_th)
plt.figure(figsize=(20,30))
plt.imshow(img)
plt.xticks([])
plt.yticks([])
plt.show()
st.markdown("<h1>Our Object Detector App using FastAPI</h1><br>", unsafe_allow_html=True)
bytesObj = st.file_uploader("Choose an image file")
st.markdown("<center><h2>or</h2></center>", unsafe_allow_html=True)
url = st.text_input('Enter URL')
if bytesObj or url:
# In streamlit we will get a bytesIO object from the file_uploader
# and we convert it to base64str for our FastAPI
if bytesObj:
base64str = bytesioObj_to_base64str(bytesObj)
elif url:
base64str = ImgURL_to_base64str(url)
# We will also create the image in PIL Image format using this base64 str
# Will use this image to show in matplotlib in streamlit
img = base64str_to_PILImage(base64str)
# Run FastAPI
payload = json.dumps({
"base64str": base64str,
"threshold": 0.5
})
response = requests.put("http://18.237.28.174/predict",data = payload)
data_dict = response.json()
st.markdown("<center><h1>App Result</h1></center>", unsafe_allow_html=True)
drawboundingbox(img, data_dict['boxes'], data_dict['classes'])
st.pyplot()
st.markdown("<center><h1>FastAPI Response</h1></center><br>", unsafe_allow_html=True)
st.write(data_dict)
We can run this streamlit app in local using:
streamlit run streamlitapp.py
And we can see our app running on our localhost:8501. Works well with user-uploaded images as well as URL based images. Here is a cat image for some of you cat enthusiasts as well.
So that’s it. We have created a whole workflow here to deploy image detection models through FastAPI on ec2 and utilizing those results in Streamlit. I hope this helps your woes around deploying models in production. You can find the code for this post as well as all my posts at my GitHub repository.
Let me know if you like this post and if you would like to include Docker or FastAPI or Streamlit in your day to day deployment needs. I am also looking to create a much detailed post on Docker so follow me up to stay tuned with my writing as well. Details below.
If you want to learn more about building and putting a Machine Learning model in production, this course on AWS for implementing Machine Learning applications promises just that.
Thanks for the read. I am going to be writing more beginner-friendly posts in the future too. Follow me up at Medium or Subscribe to my blog
Also, a small disclaimer — There might be some affiliate links in this post to relevant resources, as sharing knowledge is never a bad idea.
comments powered by Disqus