profile image

Estelle Scifo

(Graph) Data Scientist & Python developer

A simple webservice with flask

August 08, 2018

This morning, a friend of mine asked me how to write a simple webservice with python. His aim was to use it with fastai models but I will keep it simple here. For such topics, I usually use django but for a single simple webservice, it is a bit too complicated. So I give it a try with flask and here is the result.

The structure

Let’s start by defining our URL and create a simple flask app. Put the following code in app.py:

from flask import Flask, render_template, request, jsonify

app = Flask(__name__)

@app.route('/classify', methods=['GET', 'POST'])
def classify():
    if request.method == 'POST':
		# if POST method, return json response
		return jsonify({
			"some_key": "some_value",
			})
    # if method == GET, render the html form
    return render_template('index.html')


if __name__ == '__main__':
    app.run(debug=True)

Here we are. If you run flask run from this file, you’ll see an error because the index.html template is missing.

Let’s fix this error right now! We will be starting with a simple HTML form, with only two inputs:

  • File selector type=file
  • Submit button type=submit

You can put this HTML code in your templates/index.html file:

<!DOCTYPE HTML>
<html lang="en-US">
    <head>
		<meta charset="UTF-8">
		<title>Image statistics</title>
		<meta name="viewport" content="width=device-width">
		<meta name="description" content="Image statistics">
	</head>
    <body>

	<h1>Image statistics</h1>
	<h2>with flask</h2>

	
	<form method="POST" enctype="multipart/form-data" action="{{ url_for('classify') }}" > 
	
	   <div class="input-file-container">
			<label for="my-file" class="label-file">Select a file...</label>		
			<input class="input-file" id="my-file" type="file" name="image" multiple>
	    </div>
	    <p class="selected-file-name"></p>
	    <input type="submit">
	    <p><a href="" onclick="resetFile()">Reset files</a></p>
	</form>
    </body>
</html>

The submit button posts data to /classify URL, which will execute by our classifiy function thanks to the special route decorator.

Now, you can run flask and visit http://127.0.0.1:5000. You can also select some image and submit the form, you’ll see our fixed JSON response.

Starting from now, you can adapt the form and view to your needs.

Read submitted images in python

In my case, I want to get some statistics about the image. So let’s improve the view. First, we’ll have to tell flask where to download the images. For that, you’ll need to pip install Flask-Uploads and then:

from flask_uploads import UploadSet, configure_uploads, IMAGES

images_upload_set = UploadSet('images', IMAGES)
app.config['UPLOADED_IMAGES_DEST'] = 'tmp_files'
configure_uploads(app, images_upload_set)

to download image in tmp_dir in your server.

To get some statistics about the image, we will use numpy and PIL:

import os
import numpy as np

from PIL import Image

@app.route('/classify', methods=['GET', 'POST'])
def classify():
    if request.method == 'POST':
        f = request.files["image"] # form input name
		# save image on disk in tmp location
		filename = images_upload_set.save(f)
		# get full image path on local disk
		file_path = images_upload_set.path(filename)
		# do things with image...
		# e.g. here, transform to numpy array through PIL
		im = Image.open(file_path)
		array_of_image = np.array(im) # array_of_image.shape: height x width x channel
		# remove temporary file
		os.remove(file_path)
		# return json response
		return jonify({
			"file": file_path,
			"shape": array_of_image.shape,
			# you can add more informations here if you want to
			})
    # if method == GET
    # render the html form
    return render_template('index.html')

Here we go! You can test this code with several images.

Multiiple image selection

A nice feature is to be able to select and analyse multiple files. It is quite simple, firstly add multiple option to the file input. Then in the python view, replace :

uploaded_files = request.files["image"]

with:

uploaded_files = request.files.getlist("image")

and then you have to perform the analysis on each file with a loop:

 for f in uploaded_files:

Error handling

When no images are selected, you can return an empty json, or display an error in the form. For the second solution, here is a way of doing it:

def classify():
    if request.method == 'POST':
        if 'image' not in request.files:
            return render_template('index.html', error="Please select at least one image")

And the error message can be displayed on the form:

   	
   	 <form method="POST" enctype="multipart/form-data" action="{{ url_for('classify') }}" >
	
	    

Style

Finally, I added some CSS style:

body {
    background: #FCFDFD;
}
h1, h2 {
    margin-bottom: 5px;
    font-weight: bold;
    text-align: center;
    color: #205e94;
}
h2 {
    color: #4192d3;
}
form {
    width: 30%;
    margin: 4em auto 0;
    text-align:center;
}
h2 + P {
    text-align: center;
}
.input-file-container {  
}  
.input-file {
    display: none;
}
.label-file {
    display: block;
    width: 50%;
    margin: auto;
    line-height: 3.5em;
    background: #205e94;
    color: #fff;
    cursor: pointer;
}
.label-file:hover {
    background-color: #4192d3;
}
.selected-file-name {  
    font-style: italic;
    font-size: .8em;
}
a {
    color: #205e94;
    cursor: pointer;
}
a:hover {
    color: #4192d3;
}
.error {
    color: red;
}

and a small JS code to show the name of the selected file to the user before submiting the form:

var fileInput = document.querySelector( ".input-file" );
var label = document.querySelector(".label-file");
var selected_file_name_ul = document.querySelector(".selected-file-name");
var error = document.querySelector(".error");

// print file name when selected
fileInput.addEventListener( "change", function( event ) {
    label.innerHTML = "Change selected file";
    var files = this.files;
    for (var i = 0; i < files.length; i++) {
	 var li = document.createElement("li");
	 li.appendChild(document.createTextNode(files[i].name));
	 selected_file_name_ul.appendChild(li);
    }
    if (error) {
	 error.style.display = "none"; 
    };
});

Reset selected files

You’ll notice that once the multiple file selection is enabled, if you hit the select files button again, the first selected files will not be deleted but the new files are appended to them. You can add a reset button in your form, e.g. with:

<p><a href="" onclick="resetFile()">Reset files</a></p>

and the corresponding JS:

	function resetFile() {
	    const file = document.querySelector('.input-file');
	    file.value = '';
	}

Conclusion

You’ll notice we have done more than the initial requirement. If you just need the webservice, only the app.py file is needed, together with the tmp_dir directory. Final code in available here:

If you want to post image with the command line, you can use for example:

curl -X POST -H "Content-Type: multipart/form-data" \
    -F "image=@my_image.jpg" http://127.0.0.1:5000/classify

or using httpie:

http -f POST http://127.0.0.1:5000/classify image@my_image.jpg

Finally, you can download a tarball with the final files here. Have fun!