Building a Food Image Classifier Web Application in Under an Hour
Project Architecture Overview
Creating an image recognition application involves three primary stages: collecting a dataset, training a convolusional neural network, and deploying the model through a web interface. Each phase is outlined with concrete implementation steps.
1. Data Acquisition
Machine learning models require substantial example data. For a food classifier, we can automate image collection using a script that queries a search engine API.
Custom Image Crawler
The following Python class constructs search URLs, extracts image links from JSON responses, and saves the downloaded pictures into a directory tree.
import os
import re
import sys
import requests
class ImageFetcher:
BASE_URL = "https://image.baidu.com/search/acjson?"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/102.0.0.0 Safari/537.36"
}
def __init__(self):
self.query = sys.argv[1]
self.pages = int(sys.argv[2])
self.output_dir = sys.argv[3]
def _build_params(self):
param_list = []
for p in range(1, self.pages + 1):
offset = 30 * p
param_list.append(
'tn=resultjson_com&ipn=rj&ct=201326592&'
'word={0}&queryWord={0}&pn={1}&rn=30&gsm={2}'.format(
self.query, offset, hex(offset)[2:])
)
return param_list
def _resolve_image_urls(self, request_urls):
collected = []
for url in request_urls:
response_text = requests.get(url, headers=self.HEADERS).text
collected += re.findall(r'"thumbURL":"(.*?)"', response_text)
return collected
def _save_batch(self, url_list):
path = os.path.join('..', self.output_dir)
os.makedirs(path, exist_ok=True)
for idx, img_url in enumerate(url_list):
file_path = os.path.join(path, f"{idx}.jpg")
with open(file_path, "wb") as f:
f.write(requests.get(img_url, headers=self.HEADERS).content)
if (idx + 1) % 30 == 0:
print(f"Downloaded page {(idx + 1) // 30}")
def execute(self):
params = self._build_params()
request_urls = [self.BASE_URL + p for p in params]
image_urls = self._resolve_image_urls(request_urls)
self._save_batch(image_urls)
if __name__ == '__main__':
fetcher = ImageFetcher()
fetcher.execute()
Organizing the Raw Data
After running the crawler for six food categories (donut, hamburger, ice-cream, pizza, rice, tart), the collected images are split into training, validation, and test sets.
import os
import shutil
def ensure_dir(path):
if not os.path.exists(path):
os.makedirs(path)
def partition_dataset(source_root, target_root, split=(0.6, 0.2, 0.2)):
classes = os.listdir(source_root)
train_r, val_r, _ = split
for subset in ['train', 'val', 'test']:
for cls in classes:
ensure_dir(os.path.join(target_root, subset, cls))
for cls in classes:
images = os.listdir(os.path.join(source_root, cls))
n = len(images)
train_end = int(n * train_r)
val_end = int(n * (train_r + val_r))
splits_map = {
'train': images[:train_end],
'val': images[train_end:val_end],
'test': images[val_end:]
}
for subset, subset_files in splits_map.items():
print(f"Moving {cls} -> {subset}")
for fname in subset_files:
shutil.copyfile(
os.path.join(source_root, cls, fname),
os.path.join(target_root, subset, cls, fname)
)
print('Dataset split completed.')
if __name__ == '__main__':
partition_dataset('foodData', 'foodData_cnn_split')
2. Model Training
TensorFlow/Keras offers a high-level API to design and train deep learning models. A convolutional neural network (CNN) is particularly effective for image classification.
from tensorflow.keras import layers, models
from tensorflow.keras.preprocessing.image import ImageDataGenerator
import matplotlib.pyplot as plt
import os
IMG_SIZE = (224, 224)
BATCH = 30
CLASS_COUNT = 6
def build_cnn():
model = models.Sequential([
layers.Conv2D(32, (3, 3), activation='relu', input_shape=(*IMG_SIZE, 3)),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(64, (3, 3), activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(128, (3, 3), activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Conv2D(128, (3, 3), activation='relu'),
layers.MaxPooling2D((2, 2)),
layers.Flatten(),
layers.Dropout(0.15),
layers.Dense(512, activation='relu'),
layers.Dense(CLASS_COUNT, activation='softmax')
])
model.compile(
optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy']
)
return model
def create_generators(train_dir, val_dir):
augment_config = dict(
rescale=1./255,
rotation_range=40,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode='nearest'
)
train_gen = ImageDataGenerator(**augment_config)
validation_gen = ImageDataGenerator(rescale=1./255)
train_flow = train_gen.flow_from_directory(
train_dir, target_size=IMG_SIZE, batch_size=BATCH, class_mode='categorical'
)
val_flow = validation_gen.flow_from_directory(
val_dir, target_size=IMG_SIZE, batch_size=BATCH, class_mode='categorical'
)
return train_flow, val_flow
if __name__ == '__main__':
model = build_cnn()
model.summary()
train_dir = './foodData_cnn_split/train'
valid_dir = './foodData_cnn_split/val'
train_gen, valid_gen = create_generators(train_dir, valid_dir)
steps_per_epoch = (300 * CLASS_COUNT) // BATCH
val_steps = (150 * CLASS_COUNT) // BATCH
history = model.fit(
train_gen,
steps_per_epoch=steps_per_epoch,
epochs=3,
validation_data=valid_gen,
validation_steps=val_steps
)
model.save('food_classifier.h5')
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs_range = range(1, len(acc) + 1)
plt.figure()
plt.plot(epochs_range, acc, 'bo', label='Train accuracy')
plt.plot(epochs_range, val_acc, 'b', label='Validation accuracy')
plt.title('Accuracy over epochs')
plt.legend()
plt.figure()
plt.plot(epochs_range, loss, 'bo', label='Train loss')
plt.plot(epochs_range, val_loss, 'b', label='Validation loss')
plt.title('Loss over epochs')
plt.legend()
plt.show()
Key observations:
flow_from_directoryexpects class-wise subdirectories.- Data augmentation (
rotation_range,horizontal_flip, etc.) expands training variety without requiring new images. - Only a few epochs are used for demonstration; real-world applications benefit from longer training and early stopping checks.
3. Web Deployment with Flask
A minimal Flask server integrates the trained model and provides a browser interface for image uploads and predictions.
Directory Layout
project_root/web/
├── static/
│ ├── Image/
│ └── CSS/
│ └── bootstrap.min.css
├── templates/
│ └── index.html
├── food_classifier.h5
├── predict.py
└── app.py
Flask Backend (app.py)
import os
import time
from flask import Flask, request, redirect, render_template, flash, url_for
from werkzeug.utils import secure_filename
from predict import classify_image
UPLOAD_FOLDER = os.path.join('static', 'Image')
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
app = Flask(__name__)
app.config['SECRET_KEY'] = 'change-me-in-production'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def is_allowed(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
if 'file' not in request.files:
flash('Missing file part')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('No file selected')
return redirect(request.url)
if file and is_allowed(file.filename):
stored_name = str(int(time.time())) + '_' + secure_filename(file.filename)
save_path = os.path.join(app.config['UPLOAD_FOLDER'], stored_name)
file.save(save_path)
prediction, probabilities = classify_image(save_path)
flash('Upload successful')
return render_template(
'index.html',
filename=stored_name,
prediction=prediction,
probabilities=probabilities
)
else:
flash('Only png, jpg, jpeg, gif files are allowed')
return render_template('index.html')
@app.route('/display/<filename>')
def display(filename):
return redirect(url_for('static', filename='Image/' + filename), code=301)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000, debug=True)
Prediction Module (predict.py)
import numpy as np
import tensorflow as tf
from PIL import Image
LABELS = ['donut', 'hamburger', 'ice-cream', 'pizza', 'rice', 'tart']
IMG_WIDTH, IMG_HEIGHT = 224, 224
def load_model():
return tf.keras.models.load_model('food_classifier.h5')
def classify_image(filepath):
img = Image.open(filepath).resize((IMG_WIDTH, IMG_HEIGHT))
array = np.asarray(img).reshape(1, IMG_WIDTH, IMG_HEIGHT, 3)
outputs = load_model().predict(array)
index = np.argmax(outputs)
confidence = outputs[0][index] * 100
result_text = f"{LABELS[index]} ({confidence:.2f}%)"
return result_text, [round(v * 100, 2) for v in outputs[0]]
HTML Template Skeleton (index.html)
A modern Bootstrap-based form allows users to choose an image, submit it, and view both the uploaded picture and classification details.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='CSS/bootstrap.min.css') }}">
<title>Food Classifier</title>
</head>
<body>
<div class="container py-4">
<h1 class="mb-4">Food Image Classifier</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-info">
{{ messages[0] }}
</div>
{% endif %}
{% endwith %}
<form method="POST" enctype="multipart/form-data">
<div class="mb-3">
<input class="form-control" type="file" name="file" required>
</div>
<button class="btn btn-primary" type="submit">Analyze</button>
</form>
{% if filename %}
<hr>
<div class="row">
<div class="col-md-4">
<img src="{{ url_for('display', filename=filename) }}" class="img-fluid">
</div>
<div class="col-md-8">
<h3>{{ prediction }}</h3>
<ul>
{% for i in range(6) %}
<li>{{ labels[i] }}: {{ probabilities[i] }}%</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
</body>
</html>
4. Running the Application
- Install dependencies:
pip install tensorflow flask pillow - Place the
food_classifier.h5file in thewebdirectory. - Execute
python app.pyand visithttp://localhost:9000. - Upload a food photo to receive a real-time classification with confidence scores.
The entire pipeline—from data scraping to a working web interface—can be completed rapidly. While the default three-epoch training yields modest accuracy, increasing epochs and dataset size substantially improves predictions.