Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/upload images background #76

Merged
merged 2 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ python-multipart>=0.0.5
gelf-formatter==0.2.1
pyyaml>=5.4.1
user-agents==2.2.0
ua-parser
python-openstackclient
python-swiftclient
ua-parser==0.10.0
python-openstackclient==5.8.0
python-swiftclient==4.0.0
84 changes: 52 additions & 32 deletions backend/src/main.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import shutil
import os
import logging
from logging.handlers import TimedRotatingFileHandler
from datetime import datetime
import time
import json
import asyncio
from uuid import uuid4
from typing import Union
from fastapi import Request, Response, FastAPI, File, Form, UploadFile, HTTPException, Cookie
from fastapi.responses import PlainTextResponse, FileResponse
from fastapi import BackgroundTasks, Cookie, FastAPI, File, Form, HTTPException, Request, Response, UploadFile
from fastapi.responses import PlainTextResponse
from fastapi.middleware.cors import CORSMiddleware
from gelfformatter import GelfFormatter
from user_agents import parse
import swiftclient
from src.model import load_model_inference, predict_image


CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))

CLOUD_PATH = f'https://storage.gra.cloud.ovh.net/v1/' + \
'AUTH_df731a99a3264215b973b3dee70a57af/basegun-public/' + \
f'uploaded-images/{os.environ["WORKSPACE"]}/'


def init_variable(var_name: str, path: str) -> str:
"""Inits global variable for folder path

Expand Down Expand Up @@ -90,9 +94,6 @@ def setup_logs(log_dir: str) -> logging.Logger:
allow_headers=["*"],
)

# Image storage
PATH_IMGS = init_variable("PATH_IMGS", "../images")

# Logs
PATH_LOGS = init_variable("PATH_LOGS", "../logs")
logger = setup_logs(PATH_LOGS)
Expand All @@ -119,6 +120,7 @@ def setup_logs(log_dir: str) -> logging.Logger:
MODEL_VERSION = "-1"


conn = None
if "OS_USERNAME" in os.environ:
# Connection to OVH cloud
conn = swiftclient.Connection(
Expand All @@ -131,12 +133,12 @@ def setup_logs(log_dir: str) -> logging.Logger:
},
auth_version='3'
)
CLOUD_PATH = f'https://storage.gra.cloud.ovh.net/v1/' + \
'AUTH_df731a99a3264215b973b3dee70a57af/basegun-public/' + \
f'uploaded-images/{os.environ["WORKSPACE"]}/'
conn.get_account()
else:
logger.warn('Variables necessary for OVH connection not set !')

async def upload_image_ovh(content, img_name):

def upload_image_ovh(content: bytes, img_name: str):
""" Uploads an image to owh swift container basegun-public
path uploaded-images/WORKSPACE/img_name
where WORKSPACE is dev, preprod or prod
Expand All @@ -145,9 +147,39 @@ async def upload_image_ovh(content, img_name):
content (bytes): file content
img_name (str): name we want to give on ovh
"""
conn.put_object("basegun-public",
f'uploaded-images/{os.environ["WORKSPACE"]}/{img_name}',
contents=content)
num_tries = 0
LIMIT_TRIES = 5
image_path = os.path.join(CLOUD_PATH, img_name)
start = time.time()

if not conn:
logger.exception("Variables not set for using OVH swift.", extra={
"bg_error_type": "NameError"
})
return

while num_tries <= LIMIT_TRIES:
num_tries += 1
extras_logging = {
"bg_date": datetime.now().isoformat(),
"bg_upload_time": time.time()-start,
"bg_image_url": image_path
}
try:
conn.put_object("basegun-public",
f'uploaded-images/{os.environ["WORKSPACE"]}/{img_name}',
contents=content)
# if success, get out of the loop
logger.info("Upload to OVH successful", extra=extras_logging)
break
except Exception as e:
if (num_tries <= LIMIT_TRIES and e.__class__.__name__ == "ClientException"):
# we try uploading another time
time.sleep(30)
continue
else:
extras_logging["bg_error_type"] = e.__class__.__name__
logger.exception(e, extra=extras_logging)


####################
Expand Down Expand Up @@ -179,6 +211,7 @@ def logs():
async def imageupload(
request: Request,
response: Response,
background_tasks: BackgroundTasks,
image: UploadFile = File(...),
date: float = Form(...),
geolocation: str = Form(...),
Expand Down Expand Up @@ -209,18 +242,9 @@ async def imageupload(
img_name = str(uuid4()) + os.path.splitext(image.filename)[1]
img_bytes = image.file.read()

# save input image
if "OS_USERNAME" in os.environ:
# upload image to OVH Cloud
upload = asyncio.create_task(upload_image_ovh(img_bytes, img_name))
image_path = os.path.join(CLOUD_PATH, img_name)
else:
# save locally
logger.warn('Storing uploaded images locally in /tmp/basegun')
with open(os.path.join("/app/images/", img_name), "wb") as f:
f.write(img_bytes)
image_path = 'http://localhost:3000/temp/' + img_name

# upload image to OVH Cloud
background_tasks.add_task(upload_image_ovh, img_bytes, img_name)
image_path = os.path.join(CLOUD_PATH, img_name)
extras_logging["bg_image_url"] = image_path

# set user id
Expand All @@ -231,8 +255,7 @@ async def imageupload(

# send image to model for prediction
start = time.time()
prediction = asyncio.create_task(predict_image(model, img_bytes))
label, confidence = await prediction
label, confidence = predict_image(model, img_bytes)
extras_logging["bg_label"] = label
extras_logging["bg_confidence"] = confidence
extras_logging["bg_model_time"] = round(time.time()-start, 2)
Expand All @@ -243,13 +266,10 @@ async def imageupload(
else:
extras_logging["bg_confidence_level"] = "high"

if "OS_USERNAME" in os.environ:
await upload

logger.info("Identification request", extra=extras_logging)

return {
"file": image_path,
"path": image_path,
"label": label,
"confidence": confidence,
"confidence_level": extras_logging["bg_confidence_level"]
Expand Down
4 changes: 2 additions & 2 deletions backend/src/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import torch
import torchvision.models as Model
from torchvision import transforms
import time


CLASSES = ['autre_epaule', 'autre_pistolet', 'epaule_a_levier_sous_garde',
'epaule_a_percussion_silex', 'epaule_a_pompe', 'epaule_a_un_coup', 'epaule_a_verrou',
Expand Down Expand Up @@ -140,7 +140,7 @@ def prepare_input(image: Image) -> torch.Tensor:
return image.unsqueeze(0).to(device)


async def predict_image(model: Model, img: bytes) -> Union[str, float]:
def predict_image(model: Model, img: bytes) -> Union[str, float]:
"""Run the model prediction on an image

Args:
Expand Down
13 changes: 9 additions & 4 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ def test_upload_and_logs(self):
res = r.json()

# checks that the json result is as expected
self.assertEqual(set(res.keys()), set({"label", "confidence", "confidence_level", "file"}))
self.assertEqual(set(res.keys()), set({"label", "confidence", "confidence_level", "path"}))
self.assertEqual(res["label"], "revolver")
self.assertAlmostEqual(res["confidence"], 99.53, places=1)
self.assertTrue(res["confidence_level"], "high")
self.assertTrue("ovh" in res["path"])
# checks that written file is exactly the same as input file
self.assertTrue("ovh" in res["file"])
response = requests.get(res["file"])
time.sleep(30)
response = requests.get(res["path"])
with Image.open(path) as image_one:
with Image.open(BytesIO(response.content)) as image_two:
self.assertEqual(image_one.size, image_two.size)
Expand All @@ -52,7 +53,11 @@ def test_upload_and_logs(self):
# checks that the result is written in logs
r = requests.get(self.url + "/logs")
self.assertEqual(r.status_code, 200)
log = r.json()[0]
# checks the latest log "Upload to OVH"
self.assertEqual(r.json()[0]["_bg_image_url"], r.json()[1]["_bg_image_url"])
self.assertEqual(r.json()[0]["short_message"], "Upload to OVH successful")
# checks the previous log "Identification request"
log = r.json()[1]
self.assertEqual(
set(log.keys()),
set({'timestamp', '_bg_device', 'host', '_bg_model_time', 'version', '_bg_device_os', '_bg_device_family',
Expand Down
3 changes: 1 addition & 2 deletions backend/tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import unittest
import os
import numpy as np
import asyncio
from PIL import Image
from torch import Tensor
from src.model import load_model_inference, prepare_input, \
Expand Down Expand Up @@ -50,6 +49,6 @@ def test_predict_image(self):
os.path.dirname(os.path.abspath(__file__)),
"revolver.jpg")
with open(path, 'rb') as f:
res = asyncio.run(predict_image(self.model, f.read()))
res = predict_image(self.model, f.read())
self.assertEqual(res[0], "revolver")
self.assertAlmostEqual(res[1], 99.53, places=1)
3 changes: 0 additions & 3 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ services:
target: ${BUILD_TARGET:-dev}
container_name: basegun-backend
environment:
- PATH_IMGS=/app/images
- PATH_LOGS=/app/logs
- OS_USERNAME
- OS_PASSWORD
Expand All @@ -25,7 +24,6 @@ services:
ports:
- 5000:5000
volumes:
- /tmp/basegun:/app/images
- $PWD/backend/src:/app/src
- $PWD/backend/tests:/app/tests
- $PWD/backend/logs:/app/logs
Expand All @@ -44,6 +42,5 @@ services:
ports:
- 3000:3000
volumes:
- /tmp/basegun:/app/public/temp
- $PWD/frontend/src:/app/src
- /app/node_modules
1 change: 0 additions & 1 deletion docker-compose-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ services:
target: prod
container_name: basegun-backend
environment:
- PATH_IMGS=/app/images
- PATH_LOGS=/app/logs
- OS_USERNAME
- OS_PASSWORD
Expand Down
2 changes: 1 addition & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "basegun",
"version": "1.3.4",
"version": "1.3.5",
"private": true,
"scripts": {
"serve": "vite preview",
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/ResultsComponent.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div>
<div class="result">
<div class="result-image" :style="{backgroundImage:`url(${store.imgName})`}"></div>
<div class="result-image" :style="{backgroundImage:`url(${store.img})`}"></div>
<div class="fr-callout custom-callout">
<div v-if="store.confidence_level == 'low'">
<div class="callout-head">
Expand Down Expand Up @@ -109,7 +109,7 @@
},
sendFeedback(bool, event) {
const json = {
"image_url": store.imgName,
"image_url": store.imgUrl,
"feedback": bool,
"confidence": store.confidence,
"label": store.label,
Expand Down
Loading