├── .gitignore ├── Dockerfile ├── README.md ├── app.py └── images └── input.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM visionai/clouddream 2 | WORKDIR /opt/caffe 3 | RUN ./data/ilsvrc12/get_ilsvrc_aux.sh 4 | RUN pip install -r examples/web_demo/requirements.txt 5 | EXPOSE 5000 6 | ADD app.py /opt/caffe/examples/web_demo/app.py 7 | CMD python examples/web_demo/app.py 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GoogleNet Classifier 2 | ======================= 3 | 4 | This is a wrapper of the Machine Learning lib Caffe running its classifier demo with GoogleNet model pre-trained. No other dependencies than docker. 5 | 6 | ## Demo 7 | 8 | [classifier.irony.svc.tutum.io](http://classifier.irony.svc.tutum.io:5000) 9 | 10 | 11 | ## Dependencies 12 | 13 | - Docker (Use docker-machine if you are running on OSX or Windows) 14 | 15 | ## How to use 16 | 17 | Just start the docker container and map the port 5000 to a public port: 18 | 19 | docker run -it -p 5000:5000 irony/caffe-docker-classifier 20 | 21 | Open the docker ip in a web browser 22 | 23 | open http://192.168.99.100:5000 24 | 25 | or use the api: 26 | 27 | curl http://192.168.99.100:5000/classify_url?imageurl=http://lorempixel.com/400/200/animals/2/ 28 | 29 | or POST to /classify_upload 30 | 31 | ### Output: 32 | 33 | ![http://lorempixel.com/400/200/animals/2/](http://lorempixel.com/400/200/animals/2/) 34 | 35 | 36 | { 37 | "result": [ 38 | true, 39 | [ 40 | [ 41 | "gorilla", 42 | "0.42251" 43 | ], 44 | [ 45 | "baboon", 46 | "0.24627" 47 | ], 48 | [ 49 | "patas", 50 | "0.13308" 51 | ], 52 | [ 53 | "spider monkey", 54 | "0.06061" 55 | ], 56 | [ 57 | "macaque", 58 | "0.05365" 59 | ] 60 | ], 61 | [ 62 | [ 63 | "primate", 64 | "2.02654" 65 | ], 66 | [ 67 | "anthropoid ape", 68 | "1.33458" 69 | ], 70 | [ 71 | "ape", 72 | "1.30788" 73 | ], 74 | [ 75 | "monkey", 76 | "1.27961" 77 | ], 78 | [ 79 | "great ape", 80 | "1.22666" 81 | ] 82 | ], 83 | "4.565" 84 | ] 85 | } 86 | 87 | 88 | ## Next steps 89 | 90 | - Provide arguments for using different models 91 | - Test GPU optimized environment 92 | - Remove unneccessary dependencies 93 | 94 | ## Development 95 | 96 | Just clone this repo and use this command to link the local app.py to the container: 97 | 98 | docker build -t caffe . 99 | docker run -it -v $(pwd)/app.py:/opt/caffe/examples/web_demo/app.py caffe 100 | 101 | Pull requests are welcome! 102 | 103 | ## License 104 | 105 | Please read the license from the pretrained GoogleNet model here, including source ImageNet rights: 106 | http://caffe.berkeleyvision.org/model_zoo.html 107 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import cPickle 4 | import datetime 5 | import logging 6 | import flask 7 | import werkzeug 8 | import optparse 9 | import tornado.wsgi 10 | import tornado.httpserver 11 | import numpy as np 12 | import pandas as pd 13 | from PIL import Image 14 | import cStringIO as StringIO 15 | import urllib 16 | import exifutil 17 | 18 | import caffe 19 | 20 | REPO_DIRNAME = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + '/../..') 21 | UPLOAD_FOLDER = '/tmp/caffe_demos_uploads' 22 | ALLOWED_IMAGE_EXTENSIONS = set(['png', 'bmp', 'jpg', 'jpe', 'jpeg', 'gif']) 23 | 24 | # Obtain the flask app object 25 | app = flask.Flask(__name__) 26 | 27 | 28 | @app.route('/') 29 | def index(): 30 | return flask.render_template('index.html', has_result=False) 31 | 32 | 33 | @app.route('/classify_url', methods=['GET']) 34 | def classify_url(): 35 | imageurl = flask.request.args.get('imageurl', '') 36 | try: 37 | string_buffer = StringIO.StringIO( 38 | urllib.urlopen(imageurl).read()) 39 | image = caffe.io.load_image(string_buffer) 40 | 41 | except Exception as err: 42 | # For any exception we encounter in reading the image, we will just 43 | # not continue. 44 | logging.info('URL Image open error: %s', err) 45 | return flask.render_template( 46 | 'index.html', has_result=True, 47 | result=(False, 'Cannot open image from URL.') 48 | ) 49 | 50 | logging.info('Image: %s', imageurl) 51 | result = app.clf.classify_image(image) 52 | return flask.jsonify(result = result) 53 | 54 | 55 | @app.route('/classify_upload', methods=['POST']) 56 | def classify_upload(): 57 | try: 58 | # We will save the file to disk for possible data collection. 59 | imagefile = flask.request.files['imagefile'] 60 | filename_ = str(datetime.datetime.now()).replace(' ', '_') + \ 61 | werkzeug.secure_filename(imagefile.filename) 62 | filename = os.path.join(UPLOAD_FOLDER, filename_) 63 | imagefile.save(filename) 64 | logging.info('Saving to %s.', filename) 65 | image = exifutil.open_oriented_im(filename) 66 | 67 | except Exception as err: 68 | logging.info('Uploaded image open error: %s', err) 69 | return flask.render_template( 70 | 'index.html', has_result=True, 71 | result=(False, 'Cannot open uploaded image.') 72 | ) 73 | 74 | result = app.clf.classify_image(image) 75 | logging.info('Result: %s', result) 76 | 77 | return flask.jsonify(result = result) 78 | 79 | def embed_image_html(image): 80 | """Creates an image embedded in HTML base64 format.""" 81 | image_pil = Image.fromarray((255 * image).astype('uint8')) 82 | image_pil = image_pil.resize((256, 256)) 83 | string_buf = StringIO.StringIO() 84 | image_pil.save(string_buf, format='png') 85 | data = string_buf.getvalue().encode('base64').replace('\n', '') 86 | return 'data:image/png;base64,' + data 87 | 88 | 89 | def allowed_file(filename): 90 | return ( 91 | '.' in filename and 92 | filename.rsplit('.', 1)[1] in ALLOWED_IMAGE_EXTENSIONS 93 | ) 94 | 95 | 96 | class ImagenetClassifier(object): 97 | default_args = { 98 | 'model_def_file': ( 99 | '{}/models/bvlc_googlenet/deploy.prototxt'.format(REPO_DIRNAME)), 100 | 'pretrained_model_file': ( 101 | '{}/models/bvlc_googlenet/bvlc_googlenet.caffemodel'.format(REPO_DIRNAME)), 102 | 'mean_file': ( 103 | '{}/python/caffe/imagenet/ilsvrc_2012_mean.npy'.format(REPO_DIRNAME)), 104 | 'class_labels_file': ( 105 | '{}/data/ilsvrc12/synset_words.txt'.format(REPO_DIRNAME)), 106 | 'bet_file': ( 107 | '{}/data/ilsvrc12/imagenet.bet.pickle'.format(REPO_DIRNAME)), 108 | } 109 | for key, val in default_args.iteritems(): 110 | if not os.path.exists(val): 111 | raise Exception( 112 | "File for {} is missing. Should be at: {}".format(key, val)) 113 | default_args['image_dim'] = 256 114 | default_args['raw_scale'] = 255. 115 | 116 | def __init__(self, model_def_file, pretrained_model_file, mean_file, 117 | raw_scale, class_labels_file, bet_file, image_dim, gpu_mode): 118 | logging.info('Loading net and associated files...') 119 | if gpu_mode: 120 | caffe.set_mode_gpu() 121 | else: 122 | caffe.set_mode_cpu() 123 | self.net = caffe.Classifier( 124 | model_def_file, pretrained_model_file, 125 | image_dims=(image_dim, image_dim), raw_scale=raw_scale, 126 | mean=np.load(mean_file).mean(1).mean(1), channel_swap=(2, 1, 0) 127 | ) 128 | 129 | with open(class_labels_file) as f: 130 | labels_df = pd.DataFrame([ 131 | { 132 | 'synset_id': l.strip().split(' ')[0], 133 | 'name': ' '.join(l.strip().split(' ')[1:]).split(',')[0] 134 | } 135 | for l in f.readlines() 136 | ]) 137 | self.labels = labels_df.sort('synset_id')['name'].values 138 | 139 | self.bet = cPickle.load(open(bet_file)) 140 | # A bias to prefer children nodes in single-chain paths 141 | # I am setting the value to 0.1 as a quick, simple model. 142 | # We could use better psychological models here... 143 | self.bet['infogain'] -= np.array(self.bet['preferences']) * 0.1 144 | 145 | def classify_image(self, image): 146 | try: 147 | starttime = time.time() 148 | scores = self.net.predict([image], oversample=True).flatten() 149 | endtime = time.time() 150 | 151 | indices = (-scores).argsort()[:5] 152 | predictions = self.labels[indices] 153 | 154 | # In addition to the prediction text, we will also produce 155 | # the length for the progress bar visualization. 156 | meta = [ 157 | (p, '%.5f' % scores[i]) 158 | for i, p in zip(indices, predictions) 159 | ] 160 | logging.info('result: %s', str(meta)) 161 | 162 | # Compute expected information gain 163 | expected_infogain = np.dot( 164 | self.bet['probmat'], scores[self.bet['idmapping']]) 165 | expected_infogain *= self.bet['infogain'] 166 | 167 | # sort the scores 168 | infogain_sort = expected_infogain.argsort()[::-1] 169 | bet_result = [(self.bet['words'][v], '%.5f' % expected_infogain[v]) 170 | for v in infogain_sort[:5]] 171 | logging.info('bet result: %s', str(bet_result)) 172 | 173 | return (True, meta, bet_result, '%.3f' % (endtime - starttime)) 174 | 175 | except Exception as err: 176 | logging.info('Classification error: %s', err) 177 | return (False, 'Something went wrong when classifying the ' 178 | 'image. Maybe try another one?') 179 | 180 | 181 | def start_tornado(app, port=5000): 182 | http_server = tornado.httpserver.HTTPServer( 183 | tornado.wsgi.WSGIContainer(app)) 184 | http_server.listen(port) 185 | print("Tornado server starting on port {}".format(port)) 186 | tornado.ioloop.IOLoop.instance().start() 187 | 188 | 189 | def start_from_terminal(app): 190 | """ 191 | Parse command line options and start the server. 192 | """ 193 | parser = optparse.OptionParser() 194 | parser.add_option( 195 | '-d', '--debug', 196 | help="enable debug mode", 197 | action="store_true", default=False) 198 | parser.add_option( 199 | '-p', '--port', 200 | help="which port to serve content on", 201 | type='int', default=5000) 202 | parser.add_option( 203 | '-g', '--gpu', 204 | help="use gpu mode", 205 | action='store_true', default=False) 206 | 207 | opts, args = parser.parse_args() 208 | ImagenetClassifier.default_args.update({'gpu_mode': opts.gpu}) 209 | 210 | # Initialize classifier + warm start by forward for allocation 211 | app.clf = ImagenetClassifier(**ImagenetClassifier.default_args) 212 | app.clf.net.forward() 213 | 214 | if opts.debug: 215 | app.run(debug=True, host='0.0.0.0', port=opts.port) 216 | else: 217 | start_tornado(app, opts.port) 218 | 219 | 220 | if __name__ == '__main__': 221 | logging.getLogger().setLevel(logging.INFO) 222 | if not os.path.exists(UPLOAD_FOLDER): 223 | os.makedirs(UPLOAD_FOLDER) 224 | start_from_terminal(app) 225 | -------------------------------------------------------------------------------- /images/input.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irony/caffe-docker-classifier/a063229800a4b5a5445653097ddc83d722c7b984/images/input.jpg --------------------------------------------------------------------------------