diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index fed4b6a..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 4e04fc1..f54378d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ .buildlog/ .history .svn/ +*.json + # IntelliJ related *.iml @@ -74,4 +76,5 @@ # Backend-related # Don't check in virtual environment -/backend/env \ No newline at end of file +/backend/env +env/ \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/backend/main.py b/backend/main.py index 706013c..d822b36 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,8 +1,10 @@ import json from flask import Request, make_response -from posts.posts import get_posts +from posts.posts import get_posts, add_post, upload from posts.models import AddPostMetadata from posts.exceptions import CreatePostException, GetPostException, UnknownPostIdException +from posts.intelligence import _get_imagga_data +from requests import Response REQUEST_ARG_USER_ID = 'userId' REQUEST_ARG_POST_ID = 'postId' @@ -34,32 +36,32 @@ def posts(request: Request) -> Response: 'Content-Length': len(posts_json) } response = make_response((posts_json, status, headers)) - except GetPostQueryException: - error_body = { - 'errorCode': 'unknown', - 'message': 'An unknown server error occured. Try again later.' - } - response = make_response((error_body, 500)) except UnknownPostIdException: error_body = { 'errorCode': 'unknownPostId', 'message': 'The provided post does not exist.' } response = make_response((error_body, 404)) + except GetPostException: + error_body = { + 'errorCode': 'unknown', + 'message': 'An unknown server error occured. Try again later.' + } + response = make_response((error_body, 500)) except: response = _generate_server_error() - elif request.method === 'POST': + elif request.method == 'POST' and 'photo' in request.files: try: - imageUrl = request.args.get(REQUEST_ARG_IMAGE_URL) - if imageUrl is None or imageUrl == '' - add_post_metadata = AddPostMetadata() - # TODO: Handle uploads + image = request.files['photo'] + imgUrl = upload(image) + json_response = _get_imagga_data(imgUrl) + return json_response except CreatePostException: response = _generate_server_error() else: error_body = { 'errorCode': 'methodNotSupported', - 'message': 'This HTTP method is support supported by this server.' + 'message': 'This HTTP method is not supported by this server.' } response = make_response((error_body, 405)) return response @@ -80,7 +82,7 @@ def auth(request: Request) -> Response: """ # TODO: Handle request - return Response('OK', status=200) + return make_response(('OK', 200)) def _generate_server_error() -> Response: diff --git a/backend/posts/intelligence.py b/backend/posts/intelligence.py index 50a28fb..a80fb33 100644 --- a/backend/posts/intelligence.py +++ b/backend/posts/intelligence.py @@ -1,13 +1,44 @@ +from __future__ import absolute_import, division, print_function, unicode_literals +import tensorflow as tf + import requests import os import base64 +import urllib.parse from typing import List from models import PostMetadata +import json -API_BASE = 'https://api.imagga.com/v2' +import IPython.display as display +from PIL import Image +import numpy as np +import matplotlib.pyplot as plt +import pathlib -API_ENDPOINT_TAGS = f'{API_BASE}/tags' +AUTOTUNE = tf.data.experimental.AUTOTUNE +IMG_WIDTH = 299 +IMG_HEIGHT = 299 + +# TF Model Variables +# data_dir = pathlib.Path("C:/Users/Mr. Jeevs/.keras/datasets/flower_photos") +data_dir = pathlib.Path("C:/Users/Mr. Jeevs/.keras/datasets/train2017") +print(data_dir) + +image_count = len(list(data_dir.glob('*.jpg'))) +print(image_count) + +''' +CLASS_NAMES = np.array([item.name for item in data_dir.glob('*') if item.name != "LICENSE.txt"]) +print(CLASS_NAMES) + +roses = list(data_dir.glob('roses/*')) +for image_path in roses[:3]: + display.display(Image.open(str(image_path))) +''' +# Imagga Variables +API_BASE = 'https://api.imagga.com/v2' +API_ENDPOINT_TAGS = f'{API_BASE}/tags' TAG_LIMIT = 10 class PostPredictionData: @@ -23,33 +54,97 @@ def __init__(self, confidence: float, tag: str): def _get_imagga_data(image_url: str): - api_key = os.getenv('IMAGGA_API_KEY', None) + api_key = os.getenv('IMAGGA_API_KEY', None) # Get API Key From Environment Variable if api_key is None: raise Exception('API key not provided in environment variables!') hashed_key = str(base64.b64encode(api_key.encode('utf-8')), 'utf-8') - data = { - 'image': image_url, - 'limit': TAG_LIMIT, - } + url_query = urllib.parse.quote(image_url) # URL Encode Given Image URL + ''' headers = { 'Authorization': f'Basic {hashed_key}' } - response = requests.post(API_ENDPOINT_TAGS, data, headers) + ''' + access_url = f'https://api.imagga.com/v2/tags?image_url={url_query}&limit={TAG_LIMIT}' + + # TODO: Does Not Accept "hashed_key" as a parameter. Figure what "api_secret" is + response = requests.get(access_url, auth=(api_key, hashed_key)) response_json = response.json() return response_json - def generate_image_metadata(image_url: str) -> List[PostPredictionData]: + """Generate image metadata. Returns: A list of PostPredictionData containing results from hashtag generation. """ - result_json = _get_imagga_data(imageUrl) + result_json = _get_imagga_data(image_url) predictions = [] for json_object in result_json.result.tags: tag = json_object['tag']['en'] confidence = json_object['confidence'] prediction = PostPredictionData(confidence, tag) - predictions.add(prediction) + predictions.append(prediction) return predictions + +def __get__tag__data(): + with open("instances_train2017.json") as f: + train_data = json.load(f) + + images = train_data["images"] + annotations = train_data["annotations"] + + fileNames = [] + catIDs = [] + + for i in images[:10]: + fileNames.append(i["file_name"]) + for j in annotations: + if(i["id"] == j["image_id"]): + catIDs.append(j["category_id"]) + break + + print(fileNames) + print(catIDs) + + list_ds = tf.data.Dataset.from_tensor_slices((fileNames, catIDs)) + + print("8=========================================================>") + for f in list_ds.take(5): + print(f) + print("8=========================================================>") + + # Set `num_parallel_calls` so multiple images are loaded/processed in parallel. + labeled_ds = list_ds.map(process_img_label, num_parallel_calls=AUTOTUNE) + print(labeled_ds) + print("8=========================================================>") + thing = labeled_ds.take(1) + print(thing) + + m = tf.keras.Sequential([ + hub.KerasLayer("https://tfhub.dev/google/imagenet/inception_resnet_v2/classification/4", trainable=True, arguments=dict(batch_norm_momentum=0.99)) + ]) + + m.build([None, 299, 299, 3]) # Batch input shape. + +def decode_img(img): + # convert the compressed string to a 3D uint8 tensor + img = tf.image.decode_jpeg(img, channels=3) + # Use `convert_image_dtype` to convert to floats in the [0,1] range. + img = tf.image.convert_image_dtype(img, tf.float32) + # resize the image to the desired size. + return tf.image.resize(img, [IMG_WIDTH, IMG_HEIGHT]) + +def process_img_label(fileName, label): + # load the raw data from the file as a string + img = tf.io.read_file(f'C:/Users/Mr. Jeevs/.keras/datasets/train2017/{fileName}') + img = decode_img(img) + print(img) + print(fileName) + print(label) + return img, label + +def get_file_name(data): + return data["file_name"] + +__get__tag__data() \ No newline at end of file diff --git a/backend/posts/models.py b/backend/posts/models.py index 8fa8643..be36cd2 100644 --- a/backend/posts/models.py +++ b/backend/posts/models.py @@ -6,11 +6,10 @@ class PostMetadata: machine learning. """ - def __init__(self, id: str, image_url: str, hashtags: list, caption: str): + def __init__(self, id: str, image_url: str, hashtags: list): self.id = id self.image_url = image_url self.hashtags = hashtags - self.caption = caption class AddPostMetadata: diff --git a/backend/posts/posts.py b/backend/posts/posts.py index e6bdeec..b603e65 100644 --- a/backend/posts/posts.py +++ b/backend/posts/posts.py @@ -1,7 +1,13 @@ """Functions related to post management and database communcation.""" from typing import List -from firebase_admin import firestore, initialize_app + +import firebase_admin +from firebase_admin import firestore, initialize_app, credentials, storage + +from flask import Flask, render_template, request, flash, redirect, url_for +from werkzeug.utils import secure_filename + from google.cloud.exceptions import NotFound from intelligence import generate_image_metadata from models import AddPostMetadata, PostMetadata @@ -10,14 +16,15 @@ COLLECTION_POSTS = u'/posts' # Initialize Firebase with Application Default Credentials -initialize_app(name='hashpost') +initialize_app({'storageBucket': 'hashpost.appspot.com'}) +bucket = storage.bucket() db = firestore.client() +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'} -def add_post(data: AddPostMetadata) -> str: +def add_post(data: AddPostMetadata) -> PostMetadata: """Upload a post's metadata to our database. - Our database is currently Google Cloud Firestore. Args: @@ -29,15 +36,29 @@ def add_post(data: AddPostMetadata) -> str: Raises: CreatePostException if an internal server error occurred. """ + try: post_data = generate_image_metadata(data.image_url) (timestamp, doc) = db.collection(COLLECTION_POSTS) \ .add(post_data.to_json()) - # TODO: Add timestamp to document - return doc.id + return post_data except: raise CreatePostException() +def upload(image): + """Uploads a file to the bucket.""" + if image.filename == '': # If File Name is Empty + flash('No selected file') + return redirect(request.url) + if image and allowed_file(image.filename): # If Image Exists And Is An Allowed Extension + fileName = secure_filename(image.filename) # Store File Name + path = "posts/" + fileName + + blob = bucket.blob(path) # Set Destination + blob.upload_from_file(image) # Upload Image File + + imgUrl = blob.path # Get Image URL Of BLOB + return imgUrl def get_posts() -> List[PostMetadata]: """Queries all posts that match the given filters. @@ -87,3 +108,8 @@ def delete_post(post_id: str): raise UnknownPostIdException() except: raise DeletePostException() + +# Helper Functions +def allowed_file(filename): + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS