From 663ef6d80d482722d1a966bab5800d3f5e30dc5a Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 14 Jan 2024 14:53:54 +0100 Subject: [PATCH 1/3] API improvements --- cat_detection.py | 88 +++++++++++++++------------- docs.md | 9 --- basics.md => docs/basics.md | 0 docs/docs.md | 53 +++++++++++++++++ cat.png => img/cat.png | Bin cat1.jpg => img/cat1.jpg | Bin wolf.jpg => img/wolf.jpg | Bin language_label_mapper.py | 26 +++++++++ main.py | 112 +++++++++++++++++++++++++++++------- resources/en.properties | 14 +++++ resources/pl.properties | 14 +++++ validator.py | 33 +++++++++++ 12 files changed, 279 insertions(+), 70 deletions(-) delete mode 100644 docs.md rename basics.md => docs/basics.md (100%) create mode 100644 docs/docs.md rename cat.png => img/cat.png (100%) rename cat1.jpg => img/cat1.jpg (100%) rename wolf.jpg => img/wolf.jpg (100%) create mode 100644 language_label_mapper.py create mode 100644 resources/en.properties create mode 100644 resources/pl.properties create mode 100644 validator.py diff --git a/cat_detection.py b/cat_detection.py index db6ce4d..22e0206 100644 --- a/cat_detection.py +++ b/cat_detection.py @@ -1,48 +1,58 @@ from io import BytesIO -import torch -import torch.nn.functional as F +import numpy as np from PIL import Image -from torchvision.models.resnet import resnet50, ResNet50_Weights -from torchvision.transforms import transforms - -model = resnet50(weights=ResNet50_Weights.DEFAULT) - -model.eval() - -# Define the image transformations -preprocess = transforms.Compose([ - transforms.Resize(256), - transforms.CenterCrop(224), - transforms.ToTensor(), - transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), -]) +from keras.src.applications.resnet import preprocess_input, decode_predictions +from keras.applications.resnet import ResNet50 -def is_cat(image): +""" + Recognition file. + Model is ResNet50. Pretrained model to image recognition. + If model recognize cat then returns response with first ten CAT predictions. + If first prediction is not a cat then returns False. + If prediction is not a cat (is not within list_of_labels) then skips this prediction. + Format of response: + { + 'label': {label} + 'score': {score} + } +""" + + +model = ResNet50(weights='imagenet') + + +# PRIVATE Preprocess image method +def _preprocess_image(image): try: img = Image.open(BytesIO(image.read())) - - # Preprocess the image - img_t = preprocess(img) - batch_t = torch.unsqueeze(img_t, 0) - - # Make the prediction - out = model(batch_t) - - # Apply softmax to get probabilities - probabilities = F.softmax(out, dim=1) - - # Get the maximum predicted class and its probability - max_prob, max_class = torch.max(probabilities, dim=1) - max_prob = max_prob.item() - max_class = max_class.item() - - # Check if the maximum predicted class is within the range 281-285 - if 281 <= max_class <= 285: - return max_class, max_prob - else: - return max_class, None + img = img.resize((224, 224)) + img_array = np.array(img) + img_array = np.expand_dims(img_array, axis=0) + img_array = preprocess_input(img_array) + return img_array except Exception as e: - print("Error while processing the image:", e) + print(f"Error preprocessing image: {e}") return None + + +# Generate response +def _generate_response(decoded_predictions, list_of_labels): + results = {} + for i, (imagenet_id, label, score) in enumerate(decoded_predictions): + if i == 0 and label not in list_of_labels: + return None + if score < 0.01: + break + if label in list_of_labels: + results[len(results) + 1] = {"label": label, "score": round(float(score), 2)} + return results + + +# Cat detection +def detect_cat(image_file, list_of_labels): + img_array = _preprocess_image(image_file) + prediction = model.predict(img_array) + decoded_predictions = decode_predictions(prediction, top=10)[0] + return _generate_response(decoded_predictions, list_of_labels) diff --git a/docs.md b/docs.md deleted file mode 100644 index c768b85..0000000 --- a/docs.md +++ /dev/null @@ -1,9 +0,0 @@ -# Api - -Port -> 5000 - -endpoint -> /detect-cat - -Key -> 'Image' - -Value -> {UPLOADED_FILE} \ No newline at end of file diff --git a/basics.md b/docs/basics.md similarity index 100% rename from basics.md rename to docs/basics.md diff --git a/docs/docs.md b/docs/docs.md new file mode 100644 index 0000000..b5ad04a --- /dev/null +++ b/docs/docs.md @@ -0,0 +1,53 @@ +# Api + +Port -> 5000 + +endpoint -> api/v1/detect-cat + +Key -> 'Image' + +Value -> {UPLOADED_FILE} + +Flask Rest API application to cat recognition. +If request is valid then send response with results of recognition. +If key named 'Image' in body does not occur then returns 400 (BAD REQUEST). +Otherwise, returns 200 with results of recognition. +Format of response: +```json +{ + "lang": "{users_lang}", + "results": { + "{filename}": { + "isCat": "{is_cat}", + "results": { + "1": "{result}", + "2": "{result}", + "3": "{result}", + "4": "{result}", + "5": "{result}", + "6": "{result}", + "7": "{result}", + "8": "{result}", + "9": "{result}", + "10": "{result}" + } + } + }, + "errors": [ + "{error_message}", + "{error_message}" + ] +} +``` +Format of result: +```json + { + "label": "{label}", + "score": "{score}" + } +``` + +Example response: +```json + +``` \ No newline at end of file diff --git a/cat.png b/img/cat.png similarity index 100% rename from cat.png rename to img/cat.png diff --git a/cat1.jpg b/img/cat1.jpg similarity index 100% rename from cat1.jpg rename to img/cat1.jpg diff --git a/wolf.jpg b/img/wolf.jpg similarity index 100% rename from wolf.jpg rename to img/wolf.jpg diff --git a/language_label_mapper.py b/language_label_mapper.py new file mode 100644 index 0000000..cda4eba --- /dev/null +++ b/language_label_mapper.py @@ -0,0 +1,26 @@ +from jproperties import Properties + +""" + Translator method. + If everything fine then returns translated labels. + Else throws an Exception and returns untranslated labels. +""" + + +def translate(to_translate, lang): + try: + config = Properties() + + # Load properties file for given lang + with open(f"resources/{lang}.properties", 'rb') as config_file: + config.load(config_file, encoding='UTF-8') + + # Translate labels for given to_translate dictionary + for index, label_info in to_translate.items(): + label = label_info.get("label") + to_translate[index]["label"] = config.get(label).data + return to_translate, None + except Exception as e: + error_message = f"Error translating labels: {e}" + print(error_message) + return to_translate, error_message diff --git a/main.py b/main.py index 69aa61f..7266608 100644 --- a/main.py +++ b/main.py @@ -1,37 +1,105 @@ -from flask import Flask, request, jsonify, session +from flask import Flask, request, Response, json +from cat_detection import detect_cat +from language_label_mapper import translate +from validator import validate + +""" + Flask Rest API application to cat recognition. + If request is valid then send response with results of recognition. + If key named 'Image' in body does not occurred then returns 400 (BAD REQUEST). + Otherwise returns 200 with results of recognition. + Format of response: + { + "lang": {users_lang}, + "results": { + {filename}: { + "isCat": {is_cat}, + "results": { + "1": {result} + "2": {result} + "3": {result} + ... + "10" {result} + } + }, + ... + }, + errors[ + {error_message}, + {error_message}, + ... + ] + } + To see result format -> cat_detection.py +""" -from cat_detection import is_cat # Define flask app app = Flask(__name__) app.secret_key = 'secret_key' +# Available cats +list_of_labels = [ + 'lynx', + 'lion', + 'tiger', + 'cheetah', + 'leopard', + 'jaguar', + 'tabby', + 'Egyptian_cat', + 'cougar', + 'Persian_cat', + 'Siamese_cat', + 'snow_leopard', + 'tiger_cat' +] -@app.route('/detect-cat', methods=['POST']) +# Available languages +languages = {'pl', 'en'} + + +@app.route('/api/v1/detect-cat', methods=['POST']) def upload_file(): - # 'Key' in body should be named as 'image'. Type should be 'File' and in 'Value' we should upload image from disc. - file = request.files['image'] - if file.filename == '': - return jsonify({'error': "File name is empty. Please name a file."}), 400 - max_class, max_prob = is_cat(file) + # Validate request + error_messages = validate(request) - # Save result in session - session['result'] = max_class, max_prob + # If any errors occurred, return 400 (BAD REQUEST) + if len(error_messages) > 0: + errors = json.dumps( + { + 'errors': error_messages + } + ) + return Response(errors, status=400, mimetype='application/json') - # Tworzenie komunikatu na podstawie wyniku analizy zdjęcia - translator = { - 281: "tabby cat", - 282: "tiger cat", - 283: "persian cat", - 284: "siamese cat", - 285: "egyptian cat" + # Get files from request + files = request.files.getlist('image') + + # Get user's language (Value in header 'Accept-Language'). Default value is English + lang = request.accept_languages.best_match(languages, default='en') + + # Define JSON structure for results + results = { + 'lang': lang, + 'results': {}, + 'errors': [] } - if max_prob is not None: - result = f"The image is recognized as '{translator[max_class]}' with a probability of {round(max_prob * 100, 2)}%" - else: - result = f"The image is not recognized as a class within the range 281-285 ({max_class})" - return jsonify({'result': result}), 200 + # Generate results + for file in files: + predictions = detect_cat(file, list_of_labels) + if predictions is not None: + predictions, error_messages = translate(predictions, lang) + results['results'][file.filename] = { + 'isCat': False if not predictions else True, + **({'predictions': predictions} if predictions is not None else {}) + } + if error_messages is not None and predictions is None: + results['errors'].append(error_messages) + + # Send response with 200 (Success) + return Response(json.dumps(results), status=200, mimetype='application/json') if __name__ == '__main__': diff --git a/resources/en.properties b/resources/en.properties new file mode 100644 index 0000000..90327da --- /dev/null +++ b/resources/en.properties @@ -0,0 +1,14 @@ +# EN +lynx=lynx +lion=lion +tiger=tiger +cheetah=cheetah +leopard=leopard +jaguar=jaguar +tabby=tabby +Egyptian_cat=Egyptian cat +cougar=cougar +Persian_cat=Persian cat +Siamese_cat=Siamese cat +snow_leopard=snow leopard +tiger_cat=tiger cat diff --git a/resources/pl.properties b/resources/pl.properties new file mode 100644 index 0000000..cda079e --- /dev/null +++ b/resources/pl.properties @@ -0,0 +1,14 @@ +# PL +lynx=ryś +lion=lew +tiger=tygrys +cheetah=gepard +leopard=lampart +jaguar=jaguar +tabby=kot pręgowany +Egyptian_cat=kot egipski +cougar=puma +Persian_cat=kot perski +Siamese_cat=kot syjamski +snow_leopard=lampart śnieżny +tiger_cat=kot tygrysi \ No newline at end of file diff --git a/validator.py b/validator.py new file mode 100644 index 0000000..b6afca0 --- /dev/null +++ b/validator.py @@ -0,0 +1,33 @@ +import imghdr + +""" + Validation method. + If everything fine then returns empty list. + Else returns list of error messages. +""" + +# Allowed extensions +allowed_extensions = {'jpg', 'jpeg', 'png'} + + +def validate(request): + errors = [] + try: + images = request.files.getlist('image') + + # Case 1 - > request has no 'Image' Key in body + if images is None: + raise KeyError("'Image' key not found in request.") + + # Case 2 - > if some of the images has no filename + if not images or all(img.filename == '' for img in images): + raise ValueError("Value of 'Image' key is empty.") + + # Case 3 -> if some of the images has wrong extension + for img in images: + if imghdr.what(img) not in allowed_extensions: + raise ValueError(f"Given file '{img.filename}' has no allowed extension. " + f"Allowed extensions: {allowed_extensions}.") + except Exception as e: + errors.append(e.args[0]) + return errors From 695592b7b66be7d4603d76e1f92bc0b60d52fc02 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 14 Jan 2024 15:05:47 +0100 Subject: [PATCH 2/3] requirements.txt --- requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..daeb034 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask==3.0.0 +numpy==1.26.3 +pillow==10.2.0 +keras==2.15.0 +jproperties==2.1.1 \ No newline at end of file From 294fb339ed45103f9dd9f358f9ed6439492c6999 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 17 Jan 2024 19:46:35 +0100 Subject: [PATCH 3/3] bugfixes --- language_label_mapper.py | 2 +- main.py | 2 +- validator.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/language_label_mapper.py b/language_label_mapper.py index cda4eba..aeaf1a0 100644 --- a/language_label_mapper.py +++ b/language_label_mapper.py @@ -19,7 +19,7 @@ def translate(to_translate, lang): for index, label_info in to_translate.items(): label = label_info.get("label") to_translate[index]["label"] = config.get(label).data - return to_translate, None + return to_translate, [] except Exception as e: error_message = f"Error translating labels: {e}" print(error_message) diff --git a/main.py b/main.py index 7266608..b0be64d 100644 --- a/main.py +++ b/main.py @@ -95,7 +95,7 @@ def upload_file(): 'isCat': False if not predictions else True, **({'predictions': predictions} if predictions is not None else {}) } - if error_messages is not None and predictions is None: + if len(error_messages) > 1: results['errors'].append(error_messages) # Send response with 200 (Success) diff --git a/validator.py b/validator.py index b6afca0..8aa7ec5 100644 --- a/validator.py +++ b/validator.py @@ -25,7 +25,7 @@ def validate(request): # Case 3 -> if some of the images has wrong extension for img in images: - if imghdr.what(img) not in allowed_extensions: + if not img.filename.lower().endswith(('.png', '.jpg', '.jpeg')): raise ValueError(f"Given file '{img.filename}' has no allowed extension. " f"Allowed extensions: {allowed_extensions}.") except Exception as e: