Digits Recognizer using Python and React. Build backend with Flask.
It is the second part of my long journey to creating the web application for recognition handwritten digits. Here I’ll explain how to integrate our trained model into the Flask application.
Set up environment
As we’ll build the application from scratch we need to create a virtual environment in order to incapsulate all our dependencies. Let’s create python3 virtual environment.1
2python3 -m venv venv
source venv/bin/activate
Now we have the environment and we can install all needed dependencies.1
pip install flask flask-script scipy scikit-image numpy pillow
That’s all about setting up the environment.
Configure application
Create manage.py
We need the ability of running our application without setting env variables everytime. This goal can be achieved using flask-script package. At first we should create a file called manage.py1
touch manage.py
Next step is getting this manage.py ready.1
2
3
4
5
6
7
8
9
10
11
12
13
14from flask_script import Manager
from app import create_app
app = create_app()
manager = Manager(app)
def runserver():
app.run(debug=True, host='0.0.0.0', port=5000)
if __name__ == '__main__':
manager.run()
As you see there is an unknown function create_app inside the app package. Now we’ll make this function known.
I start from creating the package.1
2mkdir app
touch app/__init__.py
And then make the target function1
2
3
4
5
6from flask import Flask
def create_app():
app = Flask(__name__)
return app
It’s now our app ready and can be run by calling.1
python3 manage.py runserver
Add storage
As we’ll deal with the kNN classifier we need to remember the clusters after the training process. The simplies way is to store serialized version of the classifier in a file. Let’s create this file.1
2mkdir storage
touch storage/classifier.txt
Create settings file
Finally we need to create settings file where we’ll have the paths of the application’s base directory and the classifier storage.1
touch settings.py
And file’s content1
2
3
4import os
BASE_DIR = os.getcwd()
CLASSIFIER_STORAGE = os.path.join(BASE_DIR, 'storage/classifier.txt')
The app itself
Write views
The first step is to specify handlers for incoming requests. We’ll develop a SPA so the first handler will always return index.html page.1
2
3
4
5def create_root_view(app):
def root(path):
return render_template("index.html")
Besides this view we also must create an api endpoint for predictions.1
2
3class PredictDigitView(MethodView):
def post(self):
pass
Construct prediction logic
For our prediction view we need 2 things: kNN classifier and image processor.
Then let’s make a repo for getting and updating our classifier.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import pickle
class ClassifierRepo:
def __init__(self, storage):
self.storage = storage
def get(self):
with open(self.storage, 'rb') as out:
try:
classifier_str = out.read()
if classifier_str != '':
return pickle.loads(classifier_str)
else:
return None
except Exception:
return None
def update(self, classifier):
with open(self.storage, 'wb') as in_:
pickle.dump(classifier, in_)
Also we need a factory to create and fit our kNN model.1
2
3
4
5
6
7
8
9
10
11
12
13from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
class ClassifierFactory:
def create_with_fit(data, target):
(X_train, X_test, y_train, y_test) = train_test_split(
data, target, test_size=0.25, random_state=42
)
model = KNeighborsClassifier(n_neighbors=3)
model.fit(X_train, y_train)
return model
Image processor will be represented by the lots of functions to convert our image from data uri to the flat numpy array of the 8x8 greyscaled image with intensity form 0 to 16(like the original ones from digits dataset).1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49import numpy as np
from skimage import exposure
import base64
from PIL import Image
from io import BytesIO
def data_uri_to_image(uri):
encoded_data = uri.split(',')[1]
image = base64.b64decode(encoded_data)
return Image.open(BytesIO(image))
def replace_transparent_background(image):
image_arr = np.array(image)
alpha1 = 0
r2, g2, b2, alpha2 = 255, 255, 255, 255
red, green, blue, alpha = image_arr[:, :, 0], image_arr[:, :, 1], image_arr[:, :, 2], image_arr[:, :, 3]
mask = (alpha == alpha1)
image_arr[:, :, :4][mask] = [r2, g2, b2, alpha2]
return Image.fromarray(image_arr)
def resize_image(image):
return image.resize((8, 8), Image.ANTIALIAS)
def white_to_black(image):
image_arr = np.array(image)
image_arr[image_arr > 230] = 0
return Image.fromarray(image_arr)
def reduce_intensity(image):
image_arr = np.array(image)
image_arr = exposure.rescale_intensity(image_arr, out_range=(0, 16))
return Image.fromarray(image_arr)
def to_classifier_input_format(data_uri):
raw_image = data_uri_to_image(data_uri)
image_with_background = replace_transparent_background(raw_image).convert('L')
resized_image = resize_image(image_with_background)
inverted_image = white_to_black(resized_image)
low_intensed_image = reduce_intensity(inverted_image)
flat_image = np.array(low_intensed_image).flatten()
return np.array([flat_image])
It’s almost done. The only thing to finish our view is to combine all the things above into a service.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19from sklearn.datasets import load_digits
from app.classifier import ClassifierFactory
from app.utils import to_classifier_input_format
class PredictDigitService:
def __init__(self, repo):
self.repo = repo
def handle(self, image_data_uri):
classifier = self.repo.get()
if classifier is None:
digits = load_digits()
classifier = ClassifierFactory.create(digits.data, digits.target)
self.repo.update(classifier)
x = to_classifier_input_format(image_data_uri)
prediction = classifier.predict(x)[0]
return prediction
Use this service in our view.1
2
3
4
5
6
7class PredictDigitView(MethodView):
def post(self):
repo = ClassifierRepo(CLASSIFIER_STORAGE)
service = PredictDigitService(repo)
image_data_uri = request.json['image']
prediction = service.handle(image_data_uri)
return Response(str(prediction).encode(), status=200)
And initialize handlers by calling this function inside the create_app.1
2
3
4
5
6
7def init_urls(app):
app.add_url_rule(
'/api/predict',
view_func=PredictDigitView.as_view('predict_digit'),
methods=['POST']
)
create_root_view(app)
So the final version of __init__.py inside the app folder1
2
3
4
5
6
7
8from flask import Flask
from .urls import init_urls
def create_app():
app = Flask(__name__)
init_urls(app)
return app
Testing
Now you can test our app. Let’s run the application1
python3 manage.py runserver
And send an image in base64 format. You can do it by downloading this image, then convert it to base64 using this resource, copy the code and save it in the file called test_request.json. Now we can send this file to get a prediction.1
curl 'http://localhost:5000/api/predict' -X "POST" -H "Content-Type: application/json" -d @test_request.json -i && echo -e '\n\n'
You should see the following output.1
2
3
4
5
6
7
8
9
10(venv) Teimurs-MacBook-Pro:digits-recognizer teimurgasanov$ curl 'http://localhost:5000/api/predict' -X "POST" -H "Content-Type: application/json" -d @test_request.json -i && echo -e '\n\n'
HTTP/1.1 100 Continue
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1
Server: Werkzeug/0.14.1 Python/3.6.3
Date: Tue, 27 Mar 2018 07:02:08 GMT
4
As you see our web app correctly detected that it is 4.
Final result
You can find the code from this article in my Github repository.
To be continued
In the third and also the last part of building our digit recognizer, we’ll create a React application to draw digits and send them to our classifier.