Skip to content

Commit

Permalink
Merge pull request #25 from fquesada00/release/1.0.0
Browse files Browse the repository at this point in the history
Release/1.0.0
  • Loading branch information
OctavioSerpe authored Feb 18, 2023
2 parents 7d49d67 + 97d1aaf commit b2e27a0
Show file tree
Hide file tree
Showing 219 changed files with 20,543 additions and 944 deletions.
25 changes: 25 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[//]: # (Agregar el nombre de la tarjeta de JIRA al nombre del PR, por ejemplo: BIT-135)
## ¿Qué cambios agrega?
[//]: # (Breve explicación del feature-hotfix-hotfeature-etc que se
incluye en el PR, y los flujos afectados)

## Auto-evaluación
[//]: # (Marcar con una [x] los casos que se cumplan.)

- [ ] Agrego los tests unitarios correspondientes.
- [ ] Deployé en un scope de prueba y probé los flujos afectados.

## Screenshots
[//]: # (Si se puede, agregar. Si no es el caso, eliminar este título.)


## ¿Merge con “merge commit” o “squash”?
Esta es una pregunta muy recurrente y debemos prestar especial atención cuando estemos a punto de darle al botón, dado que podemos dañar la trazabilidad de los cambios en el repositorio. La regla general es la siguiente:
“Siempre usar merge commit, a menos que estemos introduciendo nuevos cambios, como por ejemplo un feature branch”
Ejemplos:

- feature/my-uber-feature -> develop [SQUASH] (nuevos cambios!)
- hotfix/my-fix ->release/my-release [SQUASH] (nuevos cambios!)
- hotfix/my-fix -> master [SQUASH] (nuevos cambios!)
- release/my-release -> master [MERGE]
- master -> develop (backport) [MERGE]
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Thesis - Face generator app 2021S2
## Authors
* Agustin Roca
* Nicolas Britos
* Francisco Quesada
* Octavio Serpe
* Agustin Jerusalinsky


## Intro
Expand Down
4 changes: 2 additions & 2 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
Face Generator API built with Python using StyleGAN2.

## Setup
1. Move this folder (`api`) to your home folder.
1. Add the following lines to `~/.bashrc`, changing path as needed
```bash
mv ../api $HOME/api
export API_PATH=~/pf-2022-face-generator/api
```
2. Install `virtualenv` to keep all your dependencies in one place.
```bash
Expand Down
3 changes: 2 additions & 1 deletion api/face_generator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from src.service.app import app
import uvicorn

if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)
uvicorn.run(app, host="0.0.0.0", port=5000)
2 changes: 0 additions & 2 deletions api/gunicorn.conf.py

This file was deleted.

8 changes: 0 additions & 8 deletions api/json_serializer.py

This file was deleted.

18 changes: 6 additions & 12 deletions api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
tensorflow-gpu==1.14
scipy==1.3.3
numpy==1.14.5
numpy==1.24.1
Pillow==6.2.1
requests==2.22.0
keras==2.3.1
tqdm
Flask==0.12.2
flask-cors
gunicorn==19.9.0
dlib
opencv-python
torchvision
uvicorn==0.16.0
fastapi==0.83.0
python-multipart==0.0.5
psycopg2==2.9.5
Pyro4==4.82
2 changes: 1 addition & 1 deletion api/run.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/bin/bash
~/venv/bin/gunicorn --bind 0.0.0.0:5000 --workers 2 --timeout 4800 face_generator:app
$PROJECT_PATH/api/venv/bin/uvicorn --port 5000 --workers 1 --reload --log-level info --use-colors face_generator:app
16 changes: 16 additions & 0 deletions api/src/face_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import base64
from io import BytesIO
from PIL import Image

class FaceImage:
def __init__(self, data):
self.bytes = data

@staticmethod
def from_image(image: Image):
byte_arr = BytesIO()
image.save(byte_arr, format='PNG')
return FaceImage(byte_arr.getvalue())

def to_image(self,):
return Image.open(BytesIO(base64.b64decode(self.bytes)))
261 changes: 104 additions & 157 deletions api/src/service/app.py
Original file line number Diff line number Diff line change
@@ -1,162 +1,109 @@
from json_serializer import NumpyArrayEncoder
import json
from flask import Flask, request, jsonify
from flask_cors import CORS
import numpy as np

from fastapi import FastAPI, Response, File, UploadFile, Request, status, Query
from fastapi.exceptions import RequestValidationError
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from typing import List, Union, Generic, TypeVar
import logging
from pydantic import BaseModel
from pydantic.generics import GenericModel
import os
from src.service.service import GeneratorService

app = Flask(__name__)
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False
CORS(app)

#models
T = TypeVar('T')
class ApiResponse(GenericModel, Generic[T]):
result: T

class Error(BaseModel):
error: str

class Face(BaseModel):
z: List[float]
image: str
id: Union[int, None]


class ImageResponse(Response):
media_type = 'image/png'
def render(self, content: bytes) -> bytes:
return content

class Modifiers(BaseModel):
age: int = 0
eye_distance: int = 0
eye_eyebrow_distance: int = 0
eye_ratio: int = 0
eyes_open: int = 0
gender: int = 0
lip_ratio: int = 0
mouth_open: int = 0
mouth_ratio: int = 0
nose_mouth_distance: int = 0
nose_ratio: int = 0
nose_tip: int = 0
pitch: int = 0
roll: int = 0
smile: int = 0
yaw: int = 0

service = GeneratorService()

def make_error(status_code, message):
response = jsonify({
'status': status_code,
'message': message
})
response.status_code = status_code
return response

@app.route('/hello')
def home():
return jsonify({'msg': 'hello! :)'})


@app.route('/faces')
def getFaces():
id1 = int(request.args.get('id1'))
id2 = int(request.args.get('id2'))
imgs_bytes, ids = service.get_images_from_database(id1, id2)
return jsonify({'ids': ids, 'imgs_bytes': imgs_bytes})

@app.route('/faces', methods=['POST'])
def generateFaces():
amount = request.form['amount']
if amount is not None:
amount = int(amount)
else:
amount = 1

imgs_bytes, zs = service.generate_random_images(amount)
jsonData = {'zs': zs, 'imgs_bytes': imgs_bytes}
return json.dumps(jsonData, cls=NumpyArrayEncoder)

@app.route('/transition', methods=['POST'])
def generateTransition():
id1 = int(request.form['id1'])
id2 = int(request.form['id2'])
amount = int(request.form['amount'])

imgs_bytes, zs = service.generate_transition(id1, id2, amount)
jsonData = {'zs': zs, 'imgs_bytes': imgs_bytes}
return json.dumps(jsonData, cls=NumpyArrayEncoder)

@app.route('/latentspace', methods=['POST'])
def image_to_latent_space():
base64str = request.form['file']
imgs_bytes, zs = service.base64_to_latent(base64str)
if imgs_bytes is None:
return make_error(400, 'No face found')
jsonData = {'zs': zs, 'imgs_bytes': imgs_bytes}
return json.dumps(jsonData, cls=NumpyArrayEncoder)

@app.route('/save', methods=['POST'])
def save_latent_code():
z = request.form['z']
z = z.split(',')
z = [float(num) for num in z]
z = np.array(z)
z = z.reshape((1,18,512))
id = service.save_image(z)
jsonData = {'id': id}
return json.dumps(jsonData, cls=NumpyArrayEncoder)

@app.route('/features', methods=['POST'])
def change_features():
ageAmount = request.form['ageAmount']
if ageAmount == '':
ageAmount = 0
eyeDistanceAmount = request.form['eyeDistanceAmount']
if eyeDistanceAmount == '':
eyeDistanceAmount = 0
eyeEyebrowDistanceAmount = request.form['eyeEyebrowDistanceAmount']
if eyeEyebrowDistanceAmount == '':
eyeEyebrowDistanceAmount = 0
eyeRatioAmount = request.form['eyeRatioAmount']
if eyeRatioAmount == '':
eyeRatioAmount = 0
eyesOpenAmount = request.form['eyesOpenAmount']
if eyesOpenAmount == '':
eyesOpenAmount = 0
genderAmount = request.form['genderAmount']
if genderAmount == '':
genderAmount = 0
lipRatioAmount = request.form['lipRatioAmount']
if lipRatioAmount == '':
lipRatioAmount = 0
mouthOpenAmount = request.form['mouthOpenAmount']
if mouthOpenAmount == '':
mouthOpenAmount = 0
mouthRatioAmount = request.form['mouthRatioAmount']
if mouthRatioAmount == '':
mouthRatioAmount = 0
noseMouthDistanceAmount = request.form['noseMouthDistanceAmount']
if noseMouthDistanceAmount == '':
noseMouthDistanceAmount = 0
noseRatioAmount = request.form['noseRatioAmount']
if noseRatioAmount == '':
noseRatioAmount = 0
noseTipAmount = request.form['noseTipAmount']
if noseTipAmount == '':
noseTipAmount = 0
pitchAmount = request.form['pitchAmount']
if pitchAmount == '':
pitchAmount = 0
rollAmount = request.form['rollAmount']
if rollAmount == '':
rollAmount = 0
smileAmount = request.form['smileAmount']
if smileAmount == '':
smileAmount = 0
yawAmount = request.form['yawAmount']
if yawAmount == '':
yawAmount = 0
id = request.form['id']
features_dict = {}

features_dict['age'] = float(ageAmount)
features_dict['eye_distance'] = float(eyeDistanceAmount)
features_dict['eye_eyebrow_distance'] = float(eyeEyebrowDistanceAmount)
features_dict['eye_ratio'] = float(eyeRatioAmount)
features_dict['eyes_open'] = float(eyesOpenAmount)
features_dict['gender'] = float(genderAmount)
features_dict['lip_ratio'] = float(lipRatioAmount)
features_dict['mouth_open'] = float(mouthOpenAmount)
features_dict['mouth_ratio'] = float(mouthRatioAmount)
features_dict['nose_mouth_distance'] = float(noseMouthDistanceAmount)
features_dict['nose_ratio'] = float(noseRatioAmount)
features_dict['nose_tip'] = float(noseTipAmount)
features_dict['pitch'] = float(pitchAmount)
features_dict['roll'] = float(rollAmount)
features_dict['smile'] = float(smileAmount)
features_dict['yaw'] = float(yawAmount)

img_bytes, z = service.change_features(id, features_dict)
jsonData = {'z': z, 'img_bytes': img_bytes}
return json.dumps(jsonData, cls=NumpyArrayEncoder)

@app.route('/interchange', methods=['POST'])
def mix_styles():
id1 = int(request.form['id1'])
id2 = int(request.form['id2'])

imgs_bytes, zs = service.mix_styles(id1, id2)
jsonData = {'zs': zs, 'imgs_bytes': imgs_bytes}
return json.dumps(jsonData, cls=NumpyArrayEncoder)


if __name__ == '__main__':
app.run('0.0.0.0', 5000)
app = FastAPI(responses={422: {"model": Error}})

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({"error": exc.errors()}),
)



#routes
@app.get('/faces/generate', response_model=ApiResponse[List[Face]])
def generateFaces(amount: int = 1):
return {"result":service.generate_random_images(amount)}


@app.get('/faces', response_model=ApiResponse[List[Face]])
def getFaces(tags: List[str] = Query(None)) :

return {'result':service.get_images_from_database(tags)}

@app.get('/faces/transition', response_model=ApiResponse[List[Face]])
def generateTransition(from_id: int, to_id: int, amount: int) :
return {'result':service.generate_transition(from_id, to_id, amount)}

@app.get('/faces/interchange', response_model=ApiResponse[List[Face]])
def interchangeFaces(id1: int, id2: int) :
return {'result':service.mix_styles(id1, id2)}


@app.post('/faces/image', response_model=ApiResponse[List[Face]])
def generateFaceFromImage(image: UploadFile = File()) :
return {'result':service.img_to_latent(image.file.read())}


@app.get('/faces/{id}', response_class=ImageResponse)
def getFace(id: int, response: Response):
#response.headers['Cache-Control'] =
image = service.get_image_by_id(id)
return ImageResponse(content=image, headers={'Cache-Control': 'max-age=86400'})

class SaveRequest(BaseModel):
tags: Union[List[str], None]
@app.post('/faces/{face_id}', response_model=ApiResponse[int])
def saveFaces(face_id: str, body:SaveRequest):
print("saving face")
id = service.save_image(face_id, body.tags)
return {'result':id}


@app.put('/faces/{id}', response_model=ApiResponse[Face])
def updateFace(id:int,modifiers: Modifiers):
return {'result':service.change_features(id, vars(modifiers))}

@app.get('/tags', response_model=ApiResponse[List[str]])
def getTags():
return {'result': service.get_tags()}
Loading

0 comments on commit b2e27a0

Please sign in to comment.