Skip to content

Commit 73fdf02

Browse files
authored
Add lite version of the license plate reader example (#994)
1 parent 9407f37 commit 73fdf02

File tree

10 files changed

+281
-96
lines changed

10 files changed

+281
-96
lines changed

examples/keras/document-denoiser/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Then run the following piped commands
2626
```bash
2727
curl "${ENDPOINT}" -X POST -H "Content-Type: application/json" -d '{"url":"'${IMAGE_URL}'"}' |
2828
sed 's/"//g' |
29-
base64 -d >> prediction.png
29+
base64 -d > prediction.png
3030
```
3131

3232
Once this has run, we'll see a `prediction.png` file saved to the disk. This is the result.

examples/tensorflow/license-plate-reader/README.md

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,53 @@ Out of these three models (*YOLOv3*, *CRAFT* and *CRNN*) only *YOLOv3* has been
2424

2525
The other two models, *CRAFT* and *CRNN*, can be found in [keras-ocr](https://github.com/faustomorales/keras-ocr).
2626

27-
## Deploying
27+
## Deployment - Lite Version
2828

29-
The recommended number of instances to run this smoothly on a video stream is about 12 GPU instances (2 GPU instances for *YOLOv3* and 10 for *CRNN* + *CRAFT*). `cortex.yaml` is already set up to use these 12 instances. Note: this is the optimal number of instances when using the `g4dn.xlarge` instance type. For the client to work smoothly, the number of workers per replica can be adjusted, especially for `p3` or `g4` instances, where the GPU has a lot of compute capacity.
29+
A lite version of the deployment is available with `cortex_lite.yaml`. The lite version accepts an image as input and returns an image with the recognized license plates overlayed on top. A single GPU is required for this deployment (i.e. `g4dn.xlarge`).
30+
31+
Once the cortex cluster is created, run
32+
```bash
33+
cortex deploy cortex_lite.yaml
34+
```
35+
36+
And monitor the API with
37+
```bash
38+
cortex get --watch
39+
```
40+
41+
To run an inference on the lite version, the only 3 tools you need are `curl`, `sed` and `base64`. This API expects an URL pointing to an image onto which the inferencing is done. This includes the detection of license plates with *YOLOv3* and the recognition part with *CRAFT* + *CRNN* models.
42+
43+
Export the endpoint & the image's URL by running
44+
```bash
45+
export ENDPOINT=your-api-endpoint
46+
export IMAGE_URL=https://i.imgur.com/r8xdI7P.png
47+
```
48+
49+
Then run the following piped commands
50+
```
51+
curl "${ENDPOINT}" -X POST -H "Content-Type: application/json" -d '{"url":"'${IMAGE_URL}'"}' |
52+
sed 's/"//g' |
53+
base64 -d > prediction.jpg
54+
```
55+
56+
The resulting image is the same as the one in [Verifying the Deployed APIs](#verifying-the-deployed-apis).
57+
58+
For another prediction, let's use a generic image from the web. Export [this image's URL link](https://i.imgur.com/mYuvMOs.jpg) and re-run the prediction. This is what we get.
59+
60+
![annotated sample image](https://i.imgur.com/tg1PE1E.jpg)
61+
62+
*The above prediction has the bounding boxes colored differently to distinguish them from the cars' red bodies*
63+
64+
## Deployment - Full Version
65+
66+
The recommended number of instances to run this smoothly on a video stream is about 12 GPU instances (2 GPU instances for *YOLOv3* and 10 for *CRNN* + *CRAFT*). `cortex_full.yaml` is already set up to use these 12 instances. Note: this is the optimal number of instances when using the `g4dn.xlarge` instance type. For the client to work smoothly, the number of workers per replica can be adjusted, especially for `p3` or `g4` instances, where the GPU has a lot of compute capacity.
3067

3168
If you don't have access to this many GPU-equipped instances, you could just lower the number and expect dropped frames. It will still prove the point, albeit at a much lower framerate and with higher latency. More on that [here](https://github.com/RobertLucian/cortex-license-plate-reader-client).
3269

3370
Then after the cortex cluster is created, run
3471

3572
```bash
36-
cortex deploy
73+
cortex deploy cortex_full.yaml
3774
```
3875

3976
And monitor the APIs with
@@ -42,10 +79,6 @@ And monitor the APIs with
4279
cortex get --watch
4380
```
4481

45-
## Launching the Client
46-
47-
### Verifying the Deployed APIs
48-
4982
We can run the inference on a sample image to verify that both APIs are working as expected before we move on to running the client. Here is an example image:
5083

5184
![sample image](https://i.imgur.com/r8xdI7P.png)
@@ -81,13 +114,29 @@ Once the APIs are up and running, launch the streaming client by following the i
81114

82115
## Customization/Optimization
83116

84-
### Uploading the SavedModel to S3
117+
### Uploading the Model to S3
118+
119+
The only model to upload to an S3 bucket (for Cortex to deploy) is the *YOLOv3* model. The other two models are downloaded automatically upon deploying the service.
120+
121+
If you would like to host the model from your own bucket, or if you want to fine tune the model for your needs, here's what you can do.
85122

86-
The only model that has to be uploaded to an S3 bucket (for Cortex to deploy) is the *YOLOv3* model. The other two models are downloaded automatically upon deploying the service.
123+
#### Lite Version
87124

88-
*Note: The Keras model from [here](https://github.com/experiencor/keras-yolo3) has been converted to SavedModel model instead.*
125+
Download the *Keras* model:
126+
127+
```bash
128+
wget -O license_plate.h5 "https://www.dropbox.com/s/vsvgoyricooksyv/license_plate.h5?dl=0"
129+
```
130+
131+
And then upload it to your bucket (also make sure [cortex_lite.yaml](cortex_lite.yaml) points to this bucket):
132+
133+
```bash
134+
BUCKET=my-bucket
135+
YOLO3_PATH=examples/tensorflow/license-plate-reader/yolov3_keras
136+
aws s3 cp license_plate.h5 "s3://$BUCKET/$YOLO3_PATH/model.h5"
137+
```
89138

90-
If you would like to host the model from your own bucket, or if you want to fine tune the model for your needs, you can:
139+
#### Full Version
91140

92141
Download the *SavedModel*:
93142

@@ -101,11 +150,11 @@ Unzip it:
101150
unzip yolov3.zip -d yolov3
102151
```
103152

104-
And then upload it to your bucket (also make sure [cortex.yaml](cortex.yaml) points to this bucket):
153+
And then upload it to your bucket (also make sure [cortex_full.yaml](cortex_full.yaml) points to this bucket):
105154

106155
```bash
107156
BUCKET=my-bucket
108-
YOLO3_PATH=examples/tensorflow/license-plate-reader/yolov3
157+
YOLO3_PATH=examples/tensorflow/license-plate-reader/yolov3_tf
109158
aws s3 cp yolov3/ "s3://$BUCKET/$YOLO3_PATH" --recursive
110159
```
111160

examples/tensorflow/license-plate-reader/cortex.yaml renamed to examples/tensorflow/license-plate-reader/cortex_full.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
predictor:
55
type: tensorflow
66
path: predictor_yolo.py
7-
model: s3://cortex-examples/tensorflow/license-plate-reader/yolov3
7+
model: s3://cortex-examples/tensorflow/license-plate-reader/yolov3_tf
88
signature_key: serving_default
99
config:
1010
model_config: config.json
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# WARNING: you are on the master branch, please refer to the examples on the branch that matches your `cortex version`
2+
3+
- name: license-plate-reader
4+
predictor:
5+
type: python
6+
path: predictor_lite.py
7+
config:
8+
yolov3: s3://cortex-examples/tensorflow/license-plate-reader/yolov3_keras
9+
yolov3_model_config: config.json
10+
compute:
11+
cpu: 1
12+
gpu: 1
13+
mem: 4G
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# WARNING: you are on the master branch, please refer to the examples on the branch that matches your `cortex version`
2+
3+
import boto3, base64, cv2, re, os, requests, json
4+
import keras_ocr
5+
6+
from botocore import UNSIGNED
7+
from botocore.client import Config
8+
from tensorflow.keras.models import load_model
9+
import utils.utils as utils
10+
import utils.bbox as bbox_utils
11+
import utils.preprocess as preprocess_utils
12+
13+
14+
class PythonPredictor:
15+
def __init__(self, config):
16+
# download yolov3 model
17+
bucket, key = re.match("s3://(.+?)/(.+)", config["yolov3"]).groups()
18+
s3 = boto3.client("s3", config=Config(signature_version=UNSIGNED))
19+
model_name = "model.h5"
20+
s3.download_file(bucket, os.path.join(key, model_name), model_name)
21+
22+
# load yolov3 model
23+
self.yolov3_model = load_model(model_name)
24+
25+
# get configuration for yolov3 model
26+
with open(config["yolov3_model_config"]) as json_file:
27+
data = json.load(json_file)
28+
for key in data:
29+
setattr(self, key, data[key])
30+
self.box_confidence_score = 0.8
31+
32+
# keras-ocr automatically downloads the pretrained
33+
# weights for the detector and recognizer
34+
self.recognition_model_pipeline = keras_ocr.pipeline.Pipeline()
35+
36+
def predict(self, payload):
37+
# download image
38+
img_url = payload["url"]
39+
image = preprocess_utils.get_url_image(img_url)
40+
41+
# detect the bounding boxes
42+
boxes = utils.get_yolo_boxes(
43+
self.yolov3_model,
44+
image,
45+
self.net_h,
46+
self.net_w,
47+
self.anchors,
48+
self.obj_thresh,
49+
self.nms_thresh,
50+
len(self.labels),
51+
tensorflow_model=False,
52+
)
53+
54+
# purge bounding boxes with a low confidence score
55+
aux = []
56+
for b in boxes:
57+
label = -1
58+
for i in range(len(b.classes)):
59+
if b.classes[i] > self.box_confidence_score:
60+
label = i
61+
if label >= 0:
62+
aux.append(b)
63+
boxes = aux
64+
del aux
65+
66+
# if bounding boxes have been detected
67+
dec_words = []
68+
if len(boxes) > 0:
69+
# create set of images of the detected license plates
70+
lps = []
71+
for b in boxes:
72+
lp = image[b.ymin : b.ymax, b.xmin : b.xmax]
73+
lps.append(lp)
74+
75+
# run batch inference
76+
try:
77+
prediction_groups = self.recognition_model_pipeline.recognize(lps)
78+
except ValueError:
79+
# exception can occur when the images are too small
80+
prediction_groups = []
81+
82+
# process pipeline output
83+
image_list = []
84+
for img_predictions in prediction_groups:
85+
boxes_per_image = []
86+
for predictions in img_predictions:
87+
boxes_per_image.append([predictions[0], predictions[1].tolist()])
88+
image_list.append(boxes_per_image)
89+
90+
# reorder text within detected LPs based on horizontal position
91+
dec_lps = preprocess_utils.reorder_recognized_words(image_list)
92+
for dec_lp in dec_lps:
93+
dec_words.append([word[0] for word in dec_lp])
94+
95+
# if there are no recognized LPs, then don't draw them
96+
if len(dec_words) == 0:
97+
dec_words = [[] for i in range(len(boxes))]
98+
99+
# draw predictions as overlays on the source image
100+
draw_image = bbox_utils.draw_boxes(
101+
image,
102+
boxes,
103+
overlay_text=dec_words,
104+
labels=["LP"],
105+
obj_thresh=self.box_confidence_score,
106+
)
107+
108+
# image represented in bytes
109+
byte_im = preprocess_utils.image_to_jpeg_bytes(draw_image)
110+
111+
# encode image
112+
image_enc = base64.b64encode(byte_im).decode("utf-8")
113+
114+
# image with draw boxes overlayed
115+
return image_enc

examples/tensorflow/license-plate-reader/predictor_yolo.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import numpy as np
66
import cv2
77
import pickle
8-
from utils.utils import get_yolo_boxes
9-
from utils.bbox import BoundBox
8+
import utils.utils as utils
109

1110

1211
class TensorFlowPredictor:
@@ -26,9 +25,9 @@ def predict(self, payload):
2625
image = cv2.imdecode(jpg_as_np, flags=cv2.IMREAD_COLOR)
2726

2827
# detect the bounding boxes
29-
boxes = get_yolo_boxes(
28+
boxes = utils.get_yolo_boxes(
3029
self.client,
31-
[image],
30+
image,
3231
self.net_h,
3332
self.net_w,
3433
self.anchors,

examples/tensorflow/license-plate-reader/sample_inference.py

Lines changed: 8 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,8 @@
22

33
import click, cv2, requests, pickle, base64, json
44
import numpy as np
5-
from utils.bbox import BoundBox, draw_boxes
6-
from statistics import mean
7-
8-
9-
def image_to_jpeg_nparray(image, quality=[int(cv2.IMWRITE_JPEG_QUALITY), 95]):
10-
"""
11-
Convert numpy image to jpeg numpy vector.
12-
"""
13-
is_success, im_buf_arr = cv2.imencode(".jpg", image, quality)
14-
return im_buf_arr
15-
16-
17-
def image_to_jpeg_bytes(image, quality=[int(cv2.IMWRITE_JPEG_QUALITY), 95]):
18-
"""
19-
Convert numpy image to bytes-encoded jpeg image.
20-
"""
21-
buf = image_to_jpeg_nparray(image, quality)
22-
byte_im = buf.tobytes()
23-
return byte_im
24-
25-
26-
def get_url_image(url_image):
27-
"""
28-
Get numpy image from URL image.
29-
"""
30-
resp = requests.get(url_image, stream=True).raw
31-
image = np.asarray(bytearray(resp.read()), dtype="uint8")
32-
image = cv2.imdecode(image, cv2.IMREAD_COLOR)
33-
return image
34-
35-
36-
def reorder_recognized_words(detected_images):
37-
"""
38-
Reorder the detected words in each image based on the average horizontal position of each word.
39-
Sorting them in ascending order.
40-
"""
41-
42-
reordered_images = []
43-
for detected_image in detected_images:
44-
45-
# computing the mean average position for each word
46-
mean_horizontal_positions = []
47-
for words in detected_image:
48-
box = words[1]
49-
y_positions = [point[0] for point in box]
50-
mean_y_position = mean(y_positions)
51-
mean_horizontal_positions.append(mean_y_position)
52-
indexes = np.argsort(mean_horizontal_positions)
53-
54-
# and reordering them
55-
reordered = []
56-
for index, words in zip(indexes, detected_image):
57-
reordered.append(detected_image[index])
58-
reordered_images.append(reordered)
59-
60-
return reordered_images
5+
import utils.bbox as bbox_utils
6+
import utils.preprocess as preprocess_utils
617

628

639
@click.command(
@@ -81,8 +27,8 @@ def reorder_recognized_words(detected_images):
8127
def main(img_url_src, yolov3_endpoint, crnn_endpoint, output):
8228

8329
# get the image in bytes representation
84-
image = get_url_image(img_url_src)
85-
image_bytes = image_to_jpeg_bytes(image)
30+
image = preprocess_utils.get_url_image(img_url_src)
31+
image_bytes = preprocess_utils.image_to_jpeg_bytes(image)
8632

8733
# encode image
8834
image_enc = base64.b64encode(image_bytes).decode("utf-8")
@@ -97,7 +43,7 @@ def main(img_url_src, yolov3_endpoint, crnn_endpoint, output):
9743
boxes_raw = resp.json()["boxes"]
9844
boxes = []
9945
for b in boxes_raw:
100-
box = BoundBox(*b)
46+
box = bbox_utils.BoundBox(*b)
10147
boxes.append(box)
10248

10349
# purge bounding boxes with a low confidence score
@@ -119,7 +65,7 @@ def main(img_url_src, yolov3_endpoint, crnn_endpoint, output):
11965
lps = []
12066
for b in boxes:
12167
lp = image[b.ymin : b.ymax, b.xmin : b.xmax]
122-
jpeg = image_to_jpeg_nparray(lp)
68+
jpeg = preprocess_utils.image_to_jpeg_nparray(lp)
12369
lps.append(jpeg)
12470

12571
# encode the cropped license plates
@@ -134,15 +80,15 @@ def main(img_url_src, yolov3_endpoint, crnn_endpoint, output):
13480

13581
# parse the response
13682
dec_lps = resp.json()["license-plates"]
137-
dec_lps = reorder_recognized_words(dec_lps)
83+
dec_lps = preprocess_utils.reorder_recognized_words(dec_lps)
13884
for dec_lp in dec_lps:
13985
dec_words.append([word[0] for word in dec_lp])
14086

14187
if len(dec_words) == 0:
14288
dec_words = [[] for i in range(len(boxes))]
14389

14490
# draw predictions as overlays on the source image
145-
draw_image = draw_boxes(
91+
draw_image = bbox_utils.draw_boxes(
14692
image, boxes, overlay_text=dec_words, labels=["LP"], obj_thresh=confidence_score
14793
)
14894

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# WARNING: you are on the master branch, please refer to the examples on the branch that matches your `cortex version`

0 commit comments

Comments
 (0)