Skip to content

Commit dd54617

Browse files
committed
[example] image classification web demo
1 parent e8e8292 commit dd54617

File tree

6 files changed

+421
-1
lines changed

6 files changed

+421
-1
lines changed

data/ilsvrc12/get_ilsvrc_aux.sh

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# This script downloads the imagenet example auxiliary files including:
55
# - the ilsvrc12 image mean, binaryproto
66
# - synset ids and words
7+
# - Python pickle-format data of ImageNet graph structure and relative infogain
78
# - the training splits with labels
89

910
DIR="$( cd "$(dirname "$0")" ; pwd -P )"

docs/getting_pretrained_models.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ layout: default
88
Note that unlike Caffe itself, these models are licensed for **academic research / non-commercial use only**.
99
If you have any questions, please get in touch with us.
1010

11-
This page will be updated as more models become available.
11+
*UPDATE* July 2014: we are actively working on a service for hosting user-uploaded model definition and trained weight files.
12+
Soon, the community will be able to easily contribute different architectures!
1213

1314
### ImageNet
1415

@@ -26,4 +27,6 @@ This page will be updated as more models become available.
2627
validation accuracy 57.258% and loss 1.83948.
2728
- This model obtains a top-1 accuracy 57.1% and a top-5 accuracy 80.2% on the validation set, using just the center crop. (Using the average of 10 crops, (4 + 1 center) * 2 mirror, should obtain a bit higher accuracy)
2829

30+
### Auxiliary Data
31+
2932
Additionally, you will probably eventually need some auxiliary data (mean image, synset list, etc.): run `data/ilsvrc12/get_ilsvrc_aux.sh` from the root directory to obtain it.

examples/web_demo/app.py

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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 as PILImage
14+
import cStringIO as StringIO
15+
import urllib
16+
import caffe
17+
import exifutil
18+
19+
REPO_DIRNAME = os.path.abspath(os.path.dirname(__file__) + '/../..')
20+
UPLOAD_FOLDER = '/tmp/caffe_demos_uploads'
21+
ALLOWED_IMAGE_EXTENSIONS = set(['png', 'bmp', 'jpg', 'jpe', 'jpeg', 'gif'])
22+
23+
# Obtain the flask app object
24+
app = flask.Flask(__name__)
25+
26+
27+
@app.route('/')
28+
def index():
29+
return flask.render_template('index.html', has_result=False)
30+
31+
32+
@app.route('/classify_url', methods=['GET'])
33+
def classify_url():
34+
imageurl = flask.request.args.get('imageurl', '')
35+
try:
36+
string_buffer = StringIO.StringIO(
37+
urllib.urlopen(imageurl).read())
38+
image = caffe.io.load_image(string_buffer)
39+
40+
except Exception as err:
41+
# For any exception we encounter in reading the image, we will just
42+
# not continue.
43+
logging.info('URL Image open error: %s', err)
44+
return flask.render_template(
45+
'index.html', has_result=True,
46+
result=(False, 'Cannot open image from URL.')
47+
)
48+
49+
logging.info('Image: %s', imageurl)
50+
result = app.clf.classify_image(image)
51+
return flask.render_template(
52+
'index.html', has_result=True, result=result, imagesrc=imageurl)
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+
return flask.render_template(
76+
'index.html', has_result=True, result=result,
77+
imagesrc=embed_image_html(image)
78+
)
79+
80+
81+
def embed_image_html(image):
82+
"""Creates an image embedded in HTML base64 format."""
83+
image_pil = PILImage.fromarray((255 * image).astype('uint8'))
84+
image_pil = image_pil.resize((256, 256))
85+
string_buf = StringIO.StringIO()
86+
image_pil.save(string_buf, format='png')
87+
data = string_buf.getvalue().encode('base64').replace('\n', '')
88+
return 'data:image/png;base64,' + data
89+
90+
91+
def allowed_file(filename):
92+
return (
93+
'.' in filename and
94+
filename.rsplit('.', 1)[1] in ALLOWED_IMAGE_EXTENSIONS
95+
)
96+
97+
98+
class ImagenetClassifier(object):
99+
default_args = {
100+
'model_def_file': (
101+
'{}/examples/imagenet/imagenet_deploy.prototxt'.format(REPO_DIRNAME)),
102+
'pretrained_model_file': (
103+
'{}/examples/imagenet/caffe_reference_imagenet_model'.format(REPO_DIRNAME)),
104+
'mean_file': (
105+
'{}/python/caffe/imagenet/ilsvrc_2012_mean.npy'.format(REPO_DIRNAME)),
106+
'class_labels_file': (
107+
'{}/data/ilsvrc12/synset_words.txt'.format(REPO_DIRNAME)),
108+
'bet_file': (
109+
'{}/data/ilsvrc12/imagenet.bet.pickle'.format(REPO_DIRNAME)),
110+
}
111+
for key, val in default_args.iteritems():
112+
if not os.path.exists(val):
113+
raise Exception(
114+
"File for {} is missing. Should be at: {}".format(key, val))
115+
default_args['image_dim'] = 227
116+
default_args['gpu_mode'] = True
117+
118+
def __init__(self, model_def_file, pretrained_model_file, mean_file,
119+
class_labels_file, bet_file, image_dim, gpu_mode=False):
120+
logging.info('Loading net and associated files...')
121+
self.net = caffe.Classifier(
122+
model_def_file, pretrained_model_file, input_scale=255,
123+
image_dims=(image_dim, image_dim), gpu=gpu_mode,
124+
mean_file=mean_file, channel_swap=(2, 1, 0)
125+
)
126+
127+
with open(class_labels_file) as f:
128+
labels_df = pd.DataFrame([
129+
{
130+
'synset_id': l.strip().split(' ')[0],
131+
'name': ' '.join(l.strip().split(' ')[1:]).split(',')[0]
132+
}
133+
for l in f.readlines()
134+
])
135+
self.labels = labels_df.sort('synset_id')['name'].values
136+
137+
self.bet = cPickle.load(open(bet_file))
138+
# A bias to prefer children nodes in single-chain paths
139+
# I am setting the value to 0.1 as a quick, simple model.
140+
# We could use better psychological models here...
141+
self.bet['infogain'] -= np.array(self.bet['preferences']) * 0.1
142+
143+
def classify_image(self, image):
144+
try:
145+
starttime = time.time()
146+
scores = self.net.predict([image], oversample=True).flatten()
147+
endtime = time.time()
148+
149+
indices = (-scores).argsort()[:5]
150+
predictions = self.labels[indices]
151+
152+
# In addition to the prediction text, we will also produce
153+
# the length for the progress bar visualization.
154+
meta = [
155+
(p, '%.5f' % scores[i])
156+
for i, p in zip(indices, predictions)
157+
]
158+
logging.info('result: %s', str(meta))
159+
160+
# Compute expected information gain
161+
expected_infogain = np.dot(
162+
self.bet['probmat'], scores[self.bet['idmapping']])
163+
expected_infogain *= self.bet['infogain']
164+
165+
# sort the scores
166+
infogain_sort = expected_infogain.argsort()[::-1]
167+
bet_result = [(self.bet['words'][v], '%.5f' % expected_infogain[v])
168+
for v in infogain_sort[:5]]
169+
logging.info('bet result: %s', str(bet_result))
170+
171+
return (True, meta, bet_result, '%.3f' % (endtime - starttime))
172+
173+
except Exception as err:
174+
logging.info('Classification error: %s', err)
175+
return (False, 'Something went wrong when classifying the '
176+
'image. Maybe try another one?')
177+
178+
179+
def start_tornado(app, port=5000):
180+
http_server = tornado.httpserver.HTTPServer(
181+
tornado.wsgi.WSGIContainer(app))
182+
http_server.listen(port)
183+
print("Tornado server starting on port {}".format(port))
184+
tornado.ioloop.IOLoop.instance().start()
185+
186+
187+
def start_from_terminal(app):
188+
"""
189+
Parse command line options and start the server.
190+
"""
191+
parser = optparse.OptionParser()
192+
parser.add_option(
193+
'-d', '--debug',
194+
help="enable debug mode",
195+
action="store_true", default=False)
196+
parser.add_option(
197+
'-p', '--port',
198+
help="which port to serve content on",
199+
type='int', default=5000)
200+
opts, args = parser.parse_args()
201+
202+
# Initialize classifier
203+
app.clf = ImagenetClassifier(**ImagenetClassifier.default_args)
204+
205+
if opts.debug:
206+
app.run(debug=True, host='0.0.0.0', port=opts.port)
207+
else:
208+
start_tornado(app, opts.port)
209+
210+
211+
if __name__ == '__main__':
212+
logging.getLogger().setLevel(logging.INFO)
213+
if not os.path.exists(UPLOAD_FOLDER):
214+
os.makedirs(UPLOAD_FOLDER)
215+
start_from_terminal(app)

examples/web_demo/exifutil.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""
2+
This script handles the skimage exif problem.
3+
"""
4+
5+
from PIL import Image
6+
import numpy as np
7+
8+
ORIENTATIONS = { # used in apply_orientation
9+
2: (Image.FLIP_LEFT_RIGHT,),
10+
3: (Image.ROTATE_180,),
11+
4: (Image.FLIP_TOP_BOTTOM,),
12+
5: (Image.FLIP_LEFT_RIGHT, Image.ROTATE_90),
13+
6: (Image.ROTATE_270,),
14+
7: (Image.FLIP_LEFT_RIGHT, Image.ROTATE_270),
15+
8: (Image.ROTATE_90,)
16+
}
17+
18+
19+
def open_oriented_im(im_path):
20+
im = Image.open(im_path)
21+
if hasattr(im, '_getexif'):
22+
exif = im._getexif()
23+
if exif is not None and 274 in exif:
24+
orientation = exif[274]
25+
im = apply_orientation(im, orientation)
26+
return np.asarray(im).astype(np.float32) / 255.
27+
28+
29+
def apply_orientation(im, orientation):
30+
if orientation in ORIENTATIONS:
31+
for method in ORIENTATIONS[orientation]:
32+
im = im.transpose(method)
33+
return im

examples/web_demo/readme.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
title: Web demo
3+
description: Image classification demo running as a Flask web server.
4+
category: example
5+
layout: default
6+
include_in_docs: true
7+
---
8+
9+
# Web Demo
10+
11+
## Requirements
12+
13+
The demo server requires Python with some dependencies.
14+
To make sure you have the dependencies, please run `pip install -r examples/web_demo/requirements.txt`, and also make sure that you've compiled the Python Caffe interface and that it is on your `PYTHONPATH` (see [installation instructions](/installation.html)).
15+
16+
Make sure that you have obtained the Caffe Reference ImageNet Model and the ImageNet Auxiliary Data ([instructions](/getting_pretrained_models.html)).
17+
NOTE: if you run into trouble, try re-downloading the auxiliary files.
18+
19+
## Run
20+
21+
Running `python examples/web_demo/app.py` will bring up the demo server, accessible at `http://0.0.0.0:5000`.
22+
You can enable debug mode of the web server, or switch to a different port:
23+
24+
% python examples/web_demo/app.py -h
25+
Usage: app.py [options]
26+
27+
Options:
28+
-h, --help show this help message and exit
29+
-d, --debug enable debug mode
30+
-p PORT, --port=PORT which port to serve content on

0 commit comments

Comments
 (0)