Skip to content
This repository has been archived by the owner on Apr 16, 2023. It is now read-only.

Predict app #121

Open
wants to merge 14 commits into
base: dev
Choose a base branch
from
Open
111 changes: 5 additions & 106 deletions autofocus/predict/app/app.py
Original file line number Diff line number Diff line change
@@ -1,115 +1,14 @@
import mimetypes
import os
import time
from zipfile import ZipFile
from flask import Flask

from flask import Flask, jsonify, make_response, request
from werkzeug import secure_filename
from .routes.predict import predict_route
from .routes.predict_zip import predict_zip_route

from .model import predict_multiple, predict_single
from .utils import allowed_file, filter_image_files, list_zip_files

# We are going to upload the files to the server as part of the request, so set tmp folder here.
UPLOAD_FOLDER = "/tmp/"
ALLOWED_EXTENSIONS = set(
k for k, v in mimetypes.types_map.items() if v.startswith("image/")
)

app = Flask(__name__)
app.config.from_object(__name__)
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER


@app.route("/predict", methods=["GET", "POST"])
def classify_single():
"""Classify a single image"""
if request.method == "POST":
file = request.files["file"]

if not file:
return "No file sent."

filename = secure_filename(file.filename)

if allowed_file(filename, ALLOWED_EXTENSIONS):
file_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
# this isn't super-optimal since it's saving the file to the server
file.save(file_path)

app.logger.info("Classifying image %s" % (file_path))

# Get the predictions (output of the softmax) for this image
t = time.time()
predictions = predict_single(file_path)
dt = time.time() - t
app.logger.info("Execution time: %0.2f" % (dt * 1000.0))

os.remove(file_path)

return jsonify(predictions)
else:
return "File type not allowed. File must be of type {allowed}".format(
allowed=ALLOWED_EXTENSIONS
)


@app.route("/predict_zip", methods=["GET", "POST"])
def classify_zip():
"""Classify all images from a zip file"""
if request.method == "POST":
file = request.files["file"]

if not file:
return "No file sent."

if not file.filename.split(".")[-1] == "zip":
return ".zip is the only compression format currently supported"

filename = secure_filename(file.filename)
zip_file_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
file.save(zip_file_path)

zip_file = ZipFile(zip_file_path)
zip_file_list = list_zip_files(zip_file_path)
all_images = filter_image_files(zip_file_list, ALLOWED_EXTENSIONS)

if len(all_images) == 0:
return "No image files detected in the zip file"

# loop through images
start = 0
increment = 500
all_images_len = len(all_images)

while start < all_images_len:
end = start + increment
if end > len(all_images):
end = len(all_images)

# extract filenames
curr_file_list = all_images[start:end]
for filename in curr_file_list:
zip_file.extract(filename, path=app.config["UPLOAD_FOLDER"])

curr_file_list = [
os.path.join(app.config["UPLOAD_FOLDER"], x) for x in curr_file_list
]

predictions = predict_multiple(curr_file_list)

# remove files
for curr_file in curr_file_list:
os.remove(curr_file)

return make_response(jsonify(predictions))

start = end + 1


@app.route("/hello")
def hello():
"""Just a test endpoint to make sure server is running"""
return "Hey there!\n"
app.register_blueprint(predict_route)
app.register_blueprint(predict_zip_route)


if __name__ == "__main__":
Expand Down
70 changes: 70 additions & 0 deletions autofocus/predict/app/filesystem/TemporaryFile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import os

from werkzeug import secure_filename


UPLOAD_FOLDER = "/tmp/"


class TemporaryFile:
"""
Store a file and remove it upon destruction

Parameters:
path: The path to the file
name: Secured filename (Can be empty)
"""

def __init__(self, file=None, upload_path=UPLOAD_FOLDER):
"""
Constructor of File

Save the file on the server if a file is given.

Parameters:
file: Uploaded file object from flask
upload_path: The path to upload the file
"""
self.upload_folder = upload_path
if file:
self.setFromUploadedFile(file, upload_path)

def __del__(self):
"""
Destructor of File

Remove the file from the server.
"""
os.remove(self.path)

def setFromUploadedFile(self, file, upload_path=None):
"""
Save file from uploaded file

Parameters:
file: Uploaded file object from flask
upload_path: The path to upload the file
"""
self.name = secure_filename(file.filename)
self.path = self.name
if upload_path:
self.path = os.path.join(upload_path, self.path)
file.save(self.path)

def setPath(self, path):
"""
Set the path to a saved file

Parameters:
path: Path to the file
"""
self.path = path

def getPath(self):
"""
Return the saved path

Returns:
string: Path to the file
"""
return self.path
87 changes: 87 additions & 0 deletions autofocus/predict/app/filesystem/ZipArchive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import os
from zipfile import ZipFile

from .TemporaryFile import TemporaryFile, UPLOAD_FOLDER
from ..validation.validation import allowed_file, ALLOWED_IMAGE_FILES


class ZipArchive:
"""
Archive of a zip file

This class is to store and access a zip file.

Parameters:
file: The storage of the zip file (gets removed from the os upon destructor call)
zip: Opened zip file
"""

def __init__(self, file):
"""
Constructor of ZipFile

Store the given file and open the zip file.

Parameters:
file: Uploaded file from flask
upload_folder: The folder to save the zip file
"""
self.file = TemporaryFile(file)
self.zip = ZipFile(self.file.getPath())

def listFiles(self):
"""
List all files in the zip

Returns:
array: Array of filenames
"""
return [file.filename for file in self.zip.infolist()]

def listAllImages(self, extensions=ALLOWED_IMAGE_FILES):
"""
List all image files

Lists all image files within the zip archive based on the given extensions

Parameters:
extensions: Array of allowed image extensions

Returns:
array: Array of filenames matching the extension
"""
return [file for file in self.listFiles() if allowed_file(file, extensions)]

def hasImages(self, extensions=ALLOWED_IMAGE_FILES):
"""
Check for images in the zip file

Parameters:
extensions: Array of allowed image extensions

Returns:
boolean: True if zip has images
"""
return len(self.listAllImages(extensions)) > 0

def extractAll(self, path=UPLOAD_FOLDER, members=None):
"""
Extract all the given files

Extractes all the given files and stores them as File objects.
Upon destruction of the array, files are getting removed from os.

Parameters:
path: Path to store files
members: Files to extract

Returns:
array: Array of extracted File objects
"""
self.zip.extractall(path, members)
extractedFiles = {}
for member in members:
file = TemporaryFile()
file.setPath(os.path.join(path, member))
extractedFiles[member] = file
return extractedFiles
Empty file.
29 changes: 0 additions & 29 deletions autofocus/predict/app/model.py

This file was deleted.

48 changes: 48 additions & 0 deletions autofocus/predict/app/prediction/prediction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from pathlib import Path

from fastai.vision import load_learner, open_image


MODEL_DIR = Path(__file__).resolve().parents[2] / "models"
MODEL_NAME = "multilabel_model_20190407.pkl"
model = load_learner(MODEL_DIR, MODEL_NAME)
CLASSES = model.data.classes


def predict_multiple(files):
"""
Predict probabilities of multiple files

Parameters:
files: Dict with File objects of image file

Returns:
dict: Dictionary of probabilities for each file in files
"""
predictions = {}
for key in files:
predictions[key] = predict(files[key])
return predictions


def predict(file):
"""
Predict probabilities of single file

Parameters:
file: File object of image file
"""
image = open_image(file.getPath())
# Get the predictions (output of the softmax) for this image
pred_classes, preds, probs = model.predict(image)
return getProbabilities([prob.item() for prob in probs])


def getProbabilities(probabilities):
"""
Return formated Probabilities

Returns:
dict: A dictionary of classes to probabilities
"""
return dict(zip(CLASSES, probabilities))
Empty file.
21 changes: 21 additions & 0 deletions autofocus/predict/app/routes/predict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from flask import Blueprint, jsonify, request

from ..filesystem.TemporaryFile import TemporaryFile
from ..prediction.prediction import predict
from ..validation.predict import validate_predict_request


predict_route = Blueprint("predict", __name__)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never used Blueprint objects, but I'm intrigued. Can you point me to a resource that explains how they work?



@predict_route.route("/predict", methods=["POST"])
def classify_single():
"""Classify a single image"""
# Validate request
validate_predict_request(request)

# Get File object
file = TemporaryFile(request.files["file"])

# Return ziped probabilities
return jsonify(predict(file))
Loading