├── test.jpeg ├── pickle_model.pkl ├── Images ├── az-portal-1.png └── az-portal-2.png ├── requirements.txt ├── Dockerfile ├── LICENSE ├── app.py └── README.md /test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaveVoyles/docker-flask-image-recognition-sklearn/HEAD/test.jpeg -------------------------------------------------------------------------------- /pickle_model.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaveVoyles/docker-flask-image-recognition-sklearn/HEAD/pickle_model.pkl -------------------------------------------------------------------------------- /Images/az-portal-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaveVoyles/docker-flask-image-recognition-sklearn/HEAD/Images/az-portal-1.png -------------------------------------------------------------------------------- /Images/az-portal-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DaveVoyles/docker-flask-image-recognition-sklearn/HEAD/Images/az-portal-2.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | numpy==1.14.3 3 | Pillow==5.1.0 4 | matplotlib==2.2.2 5 | scikit-learn==0.19.1 6 | scipy==1.1.0 7 | requests==2.18.4 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | RUN apt-get update -y 3 | RUN apt-get install -y python-pip python-dev build-essential && pip install --upgrade pip && pip install numpy && pip install flask && pip install scipy && pip install scikit-learn && pip install matplotlib && pip install requests && pip install pillow 4 | COPY . /app 5 | WORKDIR /app 6 | ENTRYPOINT ["python"] 7 | CMD ["app.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dave Voyles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, jsonify, request, Response 2 | from PIL import Image, ImageOps 3 | from io import BytesIO 4 | from sklearn.externals import joblib 5 | import pickle 6 | import json 7 | import pickle 8 | import numpy as np 9 | import requests 10 | 11 | # These are the possible categories (classes) which can be detected 12 | namemap = [ 13 | 'axes', 14 | 'boots', 15 | 'carabiners', 16 | 'crampons', 17 | 'gloves', 18 | 'hardshell_jackets', 19 | 'harnesses', 20 | 'helmets', 21 | 'insulated_jackets', 22 | 'pulleys', 23 | 'rope', 24 | 'tents' 25 | ] 26 | app = Flask(__name__) 27 | 28 | 29 | def resize(image): 30 | """ Resize any image to 128 x 128, which is what the model has been trained on """ 31 | 32 | base = 128 33 | width, height = image.size 34 | 35 | if width > height: 36 | wpercent = base/float(width) 37 | hsize = int((float(height) * float(wpercent))) 38 | image = image.resize((base,hsize), Image.ANTIALIAS) 39 | else: 40 | hpercent = base/float(height) 41 | wsize = int((float(width) * float(hpercent))) 42 | image = image.resize((wsize, base), Image.ANTIALIAS) 43 | 44 | newImage = Image.new('RGB', 45 | (base, base), # A4 at 72dpi 46 | (255, 255, 255)) # White 47 | 48 | position = (int( (base/2 - image.width/2) ), 0) 49 | newImage.paste(image, position) 50 | 51 | return newImage 52 | 53 | 54 | def normalize(arr): 55 | """ This means that the largest value for each attribute is 1 and the smallest value is 0. 56 | Normalization is a good technique to use when you do not know the distribution of your data 57 | or when you know the distribution is not Gaussian (a bell curve).""" 58 | 59 | arr = arr.astype('float') 60 | 61 | # Do not touch the alpha channel 62 | for i in range(3): 63 | minval = arr[...,i].min() 64 | maxval = arr[...,i].max() 65 | if minval != maxval: 66 | arr[...,i] -= minval 67 | arr[...,i] *= (255.0/(maxval-minval)) 68 | 69 | return arr 70 | 71 | 72 | def processImage(image): 73 | """ Resize and normalize the image """ 74 | 75 | image = resize(image) 76 | arr = np.array(image) 77 | new_img = Image.fromarray(normalize(arr).astype('uint8'),'RGB') 78 | 79 | return new_img 80 | 81 | 82 | @app.route('/classify', methods=['POST']) 83 | def classify(): 84 | """ Make a POST to this endpint and pass in an URL to an image in the body of the request. 85 | Swap out the model.pkl with another trained model to classify new objects. """ 86 | 87 | try: 88 | body = request.get_json() 89 | print(body) 90 | img_url = body["url"] 91 | 92 | # Get the image and show it 93 | response = requests.get(img_url) 94 | img = Image.open(BytesIO(response.content)) 95 | prcedImg = processImage(img) 96 | 97 | # Convert a 2D image into a flat array 98 | imgFeatures = np.array(prcedImg).ravel().reshape(1,-1) 99 | print(imgFeatures) 100 | 101 | model = joblib.load('pickle_model.pkl') 102 | predict = model.predict(imgFeatures) 103 | print('The image is a ', namemap[int(predict[0])]), 104 | 105 | print('image: ', namemap[int(predict[0])], predict) 106 | 107 | # Convert the integer returned from the model into the name of the class from our namemap above. 108 | # EX: 0 = axes, 1 = boots, 2 = carabiners 109 | response = json.dumps({"classification": namemap[int(predict[0])]}) 110 | return(response) 111 | 112 | except Exception as e: 113 | print(e) 114 | raise 115 | 116 | if __name__ == '__main__': 117 | app.run(debug=True,host='0.0.0.0') 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Docker container running a flask web server for image classification using Scikit-Learn 3 | 4 | #### Author(s): The team at TD Bank | Dave Voyles, MSFT | [@DaveVoyles](http://www.twitter.com/DaveVoyles) 5 | #### URL: [www.DaveVoyles.com](http://www.davevoyles.com) 6 | 7 | Create a docker container and host it in Azure with this tutorial 8 | ---------- 9 | ### Why would you want to use this? 10 | Imagine that you have built a machine learning model and want others to be able to use it. You'd have to host it somewhere, and as more users hit the endpoint with the model, you'll need to scale dynamically to assure they have a fast and consistent experience. This project includes a number of simple, yet helpful tools. 11 | 12 | **Docker** 13 | 14 | Docker is a platform that allows users to easily pack, distribute, and manage applications within containers. It's an open-source project that automates the deployment of applications inside software containers. Gone are the days of an IT professional saying "*Well, it worked on my machine.*" Not it works on all of our machines. 15 | 16 | **Flask** 17 | 18 | Flask is great for developers working on small projects that need a quick way to make a simple, Python-powered web site. It powers loads of small one-off tools, or simple web interfaces built over existing APIs. 19 | 20 | **Scikit-Learn** 21 | 22 | Scikit-Learn is a simple and efficient tools for data mining and data analysis, which is built on NumPy, SciPy, and matplotlib. It does a lot of the dirty work involved with machine learning, and allows you to quickly build models, make predicitons, and manage your data. 23 | 24 | Once your docker container is deployed, you simply make an HTTP Post request to ```:5000/classify``` with the URL of an image in the body, and the model will return a label for what it think best describes is in the image. At the moment it is trained to detect: 25 | - axes 26 | - boots 27 | - carabiners 28 | - crampons 29 | - gloves 30 | - hardshell jackets 31 | - harnesses 32 | - helmets 33 | - insulated jackets 34 | - pulleys 35 | - rope 36 | - tents 37 | 38 | ### Is this re-usable? Can I use my own trained model? 39 | Yes! 40 | 41 | You could easily train it to classify other objects, too. All of the code for this project is contained in the app.py file, and the trained model is contained in the pickle_model.pkl file. 42 | 43 | Simply replace the *pickle_model.pkl* file with a trained model of your own. 44 | 45 | *App.py* contains the code which does several things: 46 | 47 | - Resizes the image 48 | - Normalizes the image 49 | - Parses the JSON POST request you made to its endpoint 50 | - Calls the trained model 51 | - Returns the classification result 52 | 53 | The trained model, contained in *pickle_model.pkl* expects all images to be resized (128x128) and normalized, so if you're creating a new model of your own, you may want to keep this bit of code. 54 | 55 | ### Buld the image & run it locally 56 | In a terminal, navigate to the folder containing the .dockerfile. 57 | This will create a new docker image and tag it with the name of your repository, name of the image, and the version 58 | It will take a few minutes to download & install all of the required files 59 | ``` 60 | docker build -t davevoyles/docker-flask-image-recognition-sklearn:latest . 61 | ``` 62 | 63 | Run the image locally in debug mode and expose ports 5000 64 | ``` 65 | docker run -d --name docker-flask-image-recognition-sklearn -p 5000:5000 davevoyles/docker-flask-image-recognition-sklearn 66 | ``` 67 | 68 | 69 | ### Verify everything works locally, then remove the image 70 | ``` docker ps ``` 71 | 72 | 73 | ``` docker logs docker-flask-image-recognition-sklearn ``` 74 | 75 | 76 | ``` docker rm -f docker logs docker-flask-image-recognition-sklearnn ``` 77 | 78 | 79 | Push to docker hub account name/repository. This may take a few minutes 80 | ``` 81 | docker push davevoyles/docker-flask-image-recognition-sklearn 82 | ``` 83 | 84 | ### Login to Azure via CLI 85 | ``` 86 | az login 87 | ``` 88 | 89 | ### Create resource group (one time) 90 | ``` 91 | az group create -l eastus -n dv-containers-rg 92 | ``` 93 | 94 | ### Create a container in Azure 95 | Create a container in azure w/ a public IP so that we can make HTTP post requests and expose port 5000. 96 | Pull image from dockerhub *account/repository/tag* 97 | ``` 98 | az container create --resource-group dv-containers-rg --name dv-flask-container --image davevoyles/docker-flask-image-recognition-sklearn:latest --ip-address public --location eastus --ports 5000 99 | ``` 100 | 101 | Check status of container by querying the ip address. You may have to wait a few minutes for it to complete. 102 | ``` 103 | az container show --resource-group dv-containers-rg --name dv-flask-container --query ipAddress 104 | ``` 105 | 106 | It should return with something like this: 107 | 108 | ```json 109 | { 110 | "additionalProperties": {}, 111 | "dnsNameLabel": null, 112 | "fqdn": null, 113 | "ip": "40.114.107.193", 114 | "ports": [ 115 | { 116 | "additionalProperties": {}, 117 | "port": 5000, 118 | "protocol": "TCP" 119 | } 120 | ] 121 | } 122 | ``` 123 | 124 | ### View the logs for your container 125 | 126 | ``` 127 | az container logs --resource-group dv-containers-rg --name dv-flask-container 128 | ``` 129 | 130 | 131 | You can also log into the Azure portal in your browser to see if your container service is running. 132 | 133 | ![Azure Portal 1](/Images/az-portal-1.png) 134 | 135 | ![Azure Portal 2](/Images/az-portal-2.png) 136 | 137 | 138 | 139 | ## Example requests 140 | Swap the IP address listed below with your own. These will return the label of the image you passed in. For example: 141 | 142 | ```json 143 | {"classification": "insulated_jackets"} 144 | ``` 145 | 146 | ```json 147 | curl -X POST \ 148 | http://40.117.156.248:5000/classify \ 149 | -H 'Cache-Control: no-cache' \ 150 | -H 'Content-Type: application/json' \ 151 | -d '{ 152 | "url":"https://images.thenorthface.com/is/image/TheNorthFace/NF0A2VD5_KX7_hero?$638x745$" 153 | }' 154 | ``` 155 | 156 | ```json 157 | curl -X POST \ 158 | http://40.121.22.230:5000/classify \ 159 | -H 'Cache-Control: no-cache' \ 160 | -H 'Content-Type: application/json' \ 161 | -d '{ 162 | "url":"https://images.thenorthface.com/is/image/TheNorthFace/NF0A2VD5_KX7_hero?$638x745$" 163 | }' 164 | ``` 165 | 166 | ```json 167 | curl -X POST \ 168 | http://40.121.22.230:5000/classify \ 169 | -H 'Cache-Control: no-cache' \ 170 | -H 'Content-Type: application/json' \ 171 | -d '{ 172 | "url":"https://m.fortnine.ca/media/catalog/product/cache/1/image/9df78eab33525d08d6e5fb8d27136e95/catalogimages/gmax/gm45-half-helmet-matte-black-xs.jpg" 173 | }' 174 | ``` 175 | 176 | ```json 177 | curl -X POST \ 178 | http://40.121.22.230:5000/classify \ 179 | -H 'Cache-Control: no-cache' \ 180 | -H 'Content-Type: application/json' \ 181 | -d '{ 182 | "url":"https://images.sportsdirect.com/images/products/90800440_l.jpg" 183 | }' 184 | ``` 185 | 186 | ```json 187 | curl -X POST \ 188 | http://40.121.22.230:5000/classify \ 189 | -H 'Cache-Control: no-cache' \ 190 | -H 'Content-Type: application/json' \ 191 | -d '{ 192 | "url":"https://mec.imgix.net/medias/sys_master/high-res/high-res/8860680618014/5052314-SIL00.jpg?w=600&h=600&auto=format&q=60&fit=fill&bg=FFF" 193 | }' 194 | ``` 195 | 196 | 197 | ### Delete the container 198 | 199 | ``` 200 | az container delete --name dv-flask-container --resource-group dv-containers-rgt 201 | ``` 202 | --------------------------------------------------------------------------------