Creating an End-to-end Coffee Picture Classifier on Docker
By: Salman A. F. Chen
Tags: Artificial Intelligence, Convolutional Network, Machine Learning, Docker, Deep Learning

Photo by Alesia Kazantceva on Unsplash

Intro

Months ago, I started to use Fast.ai for deep learning. The course was very lightweight and intelligible. The course also covered image classification deployment, which classifies teddy bears, grizzly bears, and black bears. What I did was to modify it as a coffee classification problem. As simple as that, because I am working part-time as a barista and I love coffee so much. I just forced to code because the world demands you to code a little bit for making such great things. So, don’t call me a programmer (yes, I hate it).

I tried it locally on Docker and publicly on render.com, but I suggest you to use Docker first since render deployment costs you $7 per month. If you are new to Docker, you should install it incipiently.

Docker get started

The required packages are listed in requirement.txt

After that, let's started. If you are impatient, just clone my GitHub repo

Code

First, we will gather some beverage images. I choose some of them as classes, you can search for free images from unsplash or other sources. For better results, I suggest you collect more than 50 images, including your own coffee.

‘americano’, ‘espresso’, ‘latte’, ‘not coffee’

2. Cool! the next step is training CNN classifier. This is my notebook on google colab. You can modify the model used or playing with other techniques. This is only a baseline.

This part will save your model.pkl .

learn.export()

3. After you save the model, you can work on the Dockerfile for deployment.

FROM python:3.8-slim-buster RUN apt-get update && apt-get install -y git python3-dev gcc \

&& rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --upgrade -r requirements.txt COPY app app/ RUN python app/server.py EXPOSE 5000 CMD ["python", "app/server.py", "serve"]

This file will set your container by installing the compiler, necessary packages, running the flask application, and exposing the container port to the device port.

4. Create a folder named app, and inside the folder, a script to run the server. I named it server.py . Note that I actually placed the model directly (not in the model folder) in the repo.

Here is the server.py .

import aiohttp

import asyncio

import uvicorn

from fastai import *

from fastai.vision import *

from io import BytesIO

from starlette.applications import Starlette

from starlette.middleware.cors import CORSMiddleware

from starlette.responses import HTMLResponse, JSONResponse

from starlette.staticfiles import StaticFiles





export_file_name = 'export.pkl'



classes = ['americano', 'espresso', 'latte', 'not coffee']

path = Path(__file__).parent



app = Starlette()

app.add_middleware(CORSMiddleware, allow_origins=['*'], allow_headers=['X-Requested-With', 'Content-Type'])

app.mount('/static', StaticFiles(directory='app/static'))





async def download_file(url, dest):

if dest.exists(): return

async with aiohttp.ClientSession() as session:

async with session.get(url) as response:

data = await response.read()

with open(dest, 'wb') as f:

f.write(data)





async def setup_learner():

await download_file(export_file_url, path / export_file_name)

try:

learn = load_learner(path, export_file_name)

return learn

except RuntimeError as e:

if len(e.args) > 0 and 'CPU-only machine' in e.args[0]:

print(e)

message = "



This model was trained with an old version of fastai and will not work in a CPU environment.



Please update the fastai library in your training environment and export your model again.



See instructions for 'Returning to work' at https://course.fast.ai."

raise RuntimeError(message)

else:

raise





loop = asyncio.get_event_loop()

tasks = [asyncio.ensure_future(setup_learner())]

learn = loop.run_until_complete(asyncio.gather(*tasks))[0]

loop.close()





@app.route('/')

async def homepage(request):

html_file = path / 'view' / 'index.html'

return HTMLResponse(html_file.open().read())





@app.route('/analyze', methods=['POST'])

async def analyze(request):

img_data = await request.form()

img_bytes = await (img_data['file'].read())

img = open_image(BytesIO(img_bytes))

prediction = learn.predict(img)[0]

return JSONResponse({'result': str(prediction)})





if __name__ == '__main__':

if 'serve' in sys.argv:

uvicorn.run(app=app, host='0.0.0.0', port=5000, log_level="info")

Please take a look at the original source code (I am pessimistic whether it is runable if you copy it :D)

5. Some supporting scripts. As you can see in app folder, there are view and static . We want to put the frontend there.

In the static folder, put style.css and client.js.

Here is what inside client.js , which is linked to index.html in the view folder.

var el = x => document.getElementById(x);



// pick input file

function showPicker() {

el("file-input").click();

}



// show input file

function showPicked(input) {

el("upload-label").innerHTML = input.files[0].name;

var reader = new FileReader();

reader.onload = function(e) {

el("image-picked").src = e.target.result;

el("image-picked").className = "";

};

reader.readAsDataURL(input.files[0]);

}



// analyze button function and result

function analyze() {

var uploadFiles = el("file-input").files;

if (uploadFiles.length !== 1) alert("Please select a file to analyze!");



el("analyze-button").innerHTML = "Analyzing...";

var xhr = new XMLHttpRequest();

var loc = window.location;

xhr.open("POST", `${loc.protocol}//${loc.hostname}:${loc.port}/analyze`,

true);

xhr.onerror = function() {

alert(xhr.responseText);

};

xhr.onload = function(e) {

if (this.readyState === 4) {

var response = JSON.parse(e.target.responseText);

el("result-label").innerHTML = `This is ${response["result"]}`;

}

el("analyze-button").innerHTML = "Analyze";

};



var fileData = new FormData();

fileData.append("file", uploadFiles[0]);

xhr.send(fileData);

}

and here is the style template, style.css . Feel free to pick colors and placements!

body {

background-color: rgb(223, 208, 208);

}



.no-display {

display: none;

}



.center {

margin: auto;

padding: 10px 30px;

text-align: center;

font-size: 18px;

font-family: sans-serif;

margin-bottom: 2em;

}



.title {

font-size: 30px;

margin-top: 1em;

margin-bottom: 1em;

color: #262626;

}



.content {

margin-top: 4em;

}



.analyze {

margin-top: 5em;

}



.upload-label {

padding: 10px;

font-size: 12px;

}



.result-label {

margin-top: 2em;

padding: 10px;

font-size: 15px;

}



button.choose-file-button {

width: 200px;

height: 40px;

border-radius: 2px;

background-color: #ffffff;

border: solid 1px #262626;

font-size: 13px;

color: #262626;

}



button.analyze-button {

width: 200px;

height: 40px;

border: solid 1px #262626;

border-radius: 2px;

background-color: #262626;

font-size: 13px;

color: #ffffff;

}



button:focus {

outline: 0;

}



Then, you can play with index.html on view folder.

<html lang='en'>



<head>

<link rel="icon" type="image/png" href="./icon.png"/>

<meta charset='utf-8'>

<link rel='stylesheet' href='../static/style.css'>

<script src='../static/client.js'></script>

</head>

<body>

<div>

<div class='center'>

<div class='title'>AI Coffee Classifier ☕</div>

<p>

Use images of <strong>your coffee in a cup or glass</strong>

and our mechanical ants will identify what kind of coffee yours belong!

</p>

<div class='content'>

<div class='no-display'>

<input id='file-input'

class='no-display'

type='file'

name='file'

accept='image/*'

onchange='showPicked(this)'>

</div>



<button class='choose-file-button' type='button' onclick='showPicker()'>Select Image</button>

<div class='upload-label'>

<label id='upload-label'>No coffee chosen</label>

</div>



<div>

<img id='image-picked' class='no-display' alt='Chosen Image' height='200'>

</div>



<div class='analyze'>

<button id='analyze-button' class='analyze-button' type='button' onclick='analyze()'>What is it?</button>

</div>



<div class='result-label'>

<label id='result-label'></label>

</div>



<br>

</br>



Freshly brewed by <a href="https://github.com/salmanhiro" target="_blank">salmanhiro</a>



</div>

</body>

</html>

6. The final step! run it on Docker. First, we need to build the container. This will execute what inside the Dockerfile. After that, I want to bind container port 5000 to local port 5000.

docker build -t coffee .



docker run --rm -it -p 5000:5000 coffee

7. Enjoy! you can test it with your own coffee. Head up to http://0.0.0.0:5000 on your browser. This is mine with buymeacoffee button.

Coffee photo by Taylor Franz on Unsplash

Reference

Fast AI course, https://course19.fast.ai/. Repository for Fast AI model deployment, https://github.com/render-examples/fastai-v3.

About the Author

Salman is a cyberpunk enthusiast that loves coffee, lo-fi music, extragalactic exploration, robot, and self-driving car so much. He is working on Asis and Mata— AI personal assistant products — as an applied researcher in the field of natural language understanding and reinforcement learning-driven self-learning agent. Previously, he was a computer vision applied researcher at the same parent company — Zapps AI, a research assistant at the Department of Astronomy, Institut Teknologi Bandung, and a robotics computer vision engineer at Dago Hoogeshool.

He pursued Astronomy and Astrophysics major at Institut Teknologi Bandung, spent a physics summer school at Princeton University, and Machine Learning Summer School (MLSS) 2020 which he is proudly taught by Max Welling’s AMLab and researchers from DeepMind.

Check his profile on LinkedIn.