├── scripts ├── config.py ├── utils.py ├── convert_tinder.py ├── generate_features_async.py ├── convert_hotornot.py ├── infer.py ├── scut_cum_stats.py ├── convert_scut.py ├── compare_models.py ├── eval_hotornot.py └── train_regressor.py ├── facial_beauty_predictor ├── backbone │ ├── mtcnn │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── data │ │ │ └── mtcnn_weights.npy │ │ ├── network.py │ │ ├── layer_factory.py │ │ └── mtcnn.py │ └── facenet.py ├── server │ ├── wsgi │ │ ├── __init__.py │ │ └── gunicorn_config.py │ ├── utils │ │ └── env.py │ ├── config.py │ ├── app.py │ └── init_app.py ├── utils │ └── img.py └── worker.py ├── .gitignore ├── deploy.sh ├── Pipfile ├── LICENSE ├── README.md └── Pipfile.lock /scripts/config.py: -------------------------------------------------------------------------------- 1 | SCORE_MARGIN = 1e-2 2 | -------------------------------------------------------------------------------- /facial_beauty_predictor/backbone/mtcnn/__init__.py: -------------------------------------------------------------------------------- 1 | # based on https://github.com/ipazc/mtcnn 2 | -------------------------------------------------------------------------------- /facial_beauty_predictor/backbone/mtcnn/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidImage(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | !facial_beauty_predictor/backbone/mtcnn/data/ 3 | *.egg-info/ 4 | .DS_STORE 5 | venv 6 | .env 7 | *.pyc 8 | .venv/ 9 | -------------------------------------------------------------------------------- /facial_beauty_predictor/backbone/mtcnn/data/mtcnn_weights.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orestis-z/facial-beauty-predictor/HEAD/facial_beauty_predictor/backbone/mtcnn/data/mtcnn_weights.npy -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | aws opsworks create-deployment --stack-id $OPSWORKS_STACK_ID --layer-ids $OPSWORKS_LAYER_ID --app-id $OPSWORKS_APP_ID --command "{\"Name\":\"deploy\"}" --comment "$(git show --pretty=format:"%h %s" -s HEAD)" -------------------------------------------------------------------------------- /facial_beauty_predictor/server/wsgi/__init__.py: -------------------------------------------------------------------------------- 1 | from facial_beauty_predictor.server.app import create_app 2 | 3 | 4 | application = create_app() 5 | 6 | 7 | if __name__ == '__main__': 8 | application.run() 9 | -------------------------------------------------------------------------------- /facial_beauty_predictor/server/utils/env.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def is_main_run(): 5 | return os.environ.get("FLASK_RUN_FROM_CLI") != "true" or \ 6 | os.environ.get("WERKZEUG_RUN_MAIN") == "true" 7 | 8 | 9 | def is_reloader(): 10 | return os.environ.get("FLASK_ENV") == "development" and \ 11 | os.environ.get("WERKZEUG_RUN_MAIN") != "true" and \ 12 | os.environ.get("WERKZEUG_SERVER_FD") is not None 13 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | matplotlib = "*" 8 | 9 | [packages] 10 | tensorflow = "<2.0,>=1.15.0" 11 | numpy = "*" 12 | imageio = "*" 13 | scikit-learn = "*" 14 | joblib = "*" 15 | opencv-python = "*" 16 | scikit-image = "*" 17 | flask = "*" 18 | sentry-sdk = "*" 19 | flask-json = "*" 20 | gunicorn = "*" 21 | gevent = "*" 22 | psutil = "*" 23 | requests = "*" 24 | boto3 = "*" 25 | blinker = "*" 26 | 27 | [requires] 28 | python_version = "3.7" 29 | 30 | [scripts] 31 | gunicorn-dev = "gunicorn facial_beauty_predictor.server.wsgi -k gevent -b localhost:5001 -p master.pid -c facial_beauty_predictor/server/wsgi/gunicorn_config.py --reload" 32 | deploy = "sh deploy.sh" 33 | -------------------------------------------------------------------------------- /scripts/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | from collections import ChainMap 3 | 4 | 5 | def normalize_dataset(db): 6 | if db["meta"]["ethnicities"]: 7 | perms = [e + g for e in db["meta"]["ethnicities"] 8 | for g in db["meta"]["genders"]] 9 | else: 10 | perms = db["meta"]["genders"] 11 | db_dict_list = [db["data"][key.lower()] for key in perms] 12 | len_min = min([len(el) for el in db_dict_list]) 13 | db_dict_list_norm = [{key: el[key] for key in random.sample( 14 | list(el), len_min)} for el in db_dict_list] 15 | data = {key: db_dict_list_norm[i] for i, key in enumerate(perms)} 16 | data.update(dict( 17 | all=ChainMap(*db_dict_list_norm), 18 | train=db["data"]["train"], 19 | test=db["data"]["test"])) 20 | return dict(data=data, meta=db["meta"]) 21 | -------------------------------------------------------------------------------- /facial_beauty_predictor/server/config.py: -------------------------------------------------------------------------------- 1 | class Config(object): 2 | FLASK_ENV_SHORTNAME = "none" 3 | DEBUG = False 4 | TESTING = False 5 | 6 | LOG_METRICS = True 7 | LOG_METRICS_TIMEOUT = 5 * 60 # [s] == 5 min 8 | 9 | FACENET_MODEL_PATH = "s3://facial-beauty-predictor/facenet/20170512-110547" 10 | REGRESSOR_MODEL_PATH = "s3://facial-beauty-predictor/regressor_model.pkl" 11 | PERCENTILS_PATH = "s3://facial-beauty-predictor/percentiles.npy" 12 | 13 | 14 | class DevelopmentConfig(Config): 15 | FLASK_ENV_SHORTNAME = "dev" 16 | DEBUG = True 17 | 18 | # LOG_METRICS_TIMEOUT = 1 * 5 # [s] 19 | 20 | # REGRESSOR_MODEL_PATH = "data/scut/mtcnn-facenet/models/sklearn.linear_model.base.LinearRegression_1.pkl" 21 | PERCENTILS_PATH = "data/percentiles.npy" 22 | 23 | 24 | class StagingConfig(Config): 25 | FLASK_ENV_SHORTNAME = "stage" 26 | DEBUG = True 27 | 28 | 29 | class ProductionConfig(Config): 30 | FLASK_ENV_SHORTNAME = "prod" 31 | -------------------------------------------------------------------------------- /facial_beauty_predictor/server/wsgi/gunicorn_config.py: -------------------------------------------------------------------------------- 1 | from os import kill, getenv 2 | from signal import SIGINT 3 | from psutil import Process 4 | from threading import Timer 5 | 6 | 7 | MAX_RSS = 1000 # [MB] 8 | MEM_CHECK_INTERVAL = 5 * 60 # [s] 9 | 10 | workers = 1 11 | timeout = 180 12 | graceful_timeout = 10 if getenv("FLASK_ENV") == "production" else 0 13 | log_level = "INFO" 14 | worker_class = "gevent" 15 | 16 | 17 | def post_worker_init(worker): 18 | process = Process(worker.pid) 19 | 20 | def mem_monitor(): 21 | rss = process.memory_info().rss / 1000 / 1000 # [MB] 22 | if rss > MAX_RSS: 23 | sig = SIGINT 24 | worker.log.info( 25 | "Workers RSS > Max RSS ({:.2f} MB > {:.2f} MB)".format(rss, MAX_RSS)) 26 | worker.log.info( 27 | "Suicide with signal {}".format(sig)) 28 | kill(worker.pid, sig) 29 | 30 | t = Timer(MEM_CHECK_INTERVAL, mem_monitor) 31 | t.name = "WorkerMemoryMonitorTimer" 32 | t.daemon = True 33 | t.start() 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Orestis Zambounis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /facial_beauty_predictor/utils/img.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def prewhiten(x): 5 | mean = np.mean(x) 6 | std = np.std(x) 7 | std_adj = np.maximum(std, 1.0 / np.sqrt(x.size)) 8 | y = np.multiply(np.subtract(x, mean), 1 / std_adj) 9 | return y 10 | 11 | 12 | def crop(image, random_crop, image_size): 13 | if image.shape[1] > image_size: 14 | sz1 = int(image.shape[1] // 2) 15 | sz2 = int(image_size // 2) 16 | if random_crop: 17 | diff = sz1 - sz2 18 | (h, v) = (np.random.randint(- diff, diff + 1), 19 | np.random.randint(- diff, diff + 1)) 20 | else: 21 | (h, v) = (0, 0) 22 | image = image[(sz1 - sz2 + v):(sz1 + sz2 + v), 23 | (sz1-sz2 + h):(sz1 + sz2 + h), :] 24 | return image 25 | 26 | 27 | def flip(image, random_flip): 28 | if random_flip and np.random.choice([True, False]): 29 | image = np.fliplr(image) 30 | return image 31 | 32 | 33 | def to_rgb(img): 34 | w, h = img.shape 35 | ret = np.empty((w, h, 3), dtype=np.uint8) 36 | ret[:, :, 0] = ret[:, :, 1] = ret[:, :, 2] = img 37 | return ret 38 | -------------------------------------------------------------------------------- /scripts/convert_tinder.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import argparse 4 | 5 | 6 | # db_dict = {split: {profile_id: {img_paths}}} 7 | 8 | 9 | def main(args): 10 | directories = os.listdir(args.db_dir) 11 | 12 | db = {} 13 | for directory in directories: 14 | if directory[0] != ".": 15 | img_paths = [] 16 | for sub_dir in os.listdir(os.path.join(args.db_dir, directory)): 17 | if sub_dir[0] != ".": 18 | sub_sub_dir = os.path.join( 19 | args.db_dir, directory, sub_dir, "processed_files") 20 | files = os.listdir(sub_sub_dir) 21 | file_640 = None 22 | for file in files: 23 | if "640" in file: 24 | file_640 = file 25 | if file_640 is not None: 26 | img_paths.append(os.path.join(sub_sub_dir, file_640)) 27 | db[directory] = dict(img_paths=img_paths) 28 | db_dict = dict(all=db) 29 | pickle.dump(db_dict, open(os.path.join( 30 | args.output_dir, "tinder.pkl"), "wb")) 31 | 32 | 33 | def parse_args(): 34 | parser = argparse.ArgumentParser() 35 | parser.add_argument( 36 | '--db-dir', 37 | dest='db_dir', 38 | type=str 39 | ) 40 | parser.add_argument( 41 | '--output-dir', 42 | dest='output_dir', 43 | default='data', 44 | type=str 45 | ) 46 | return parser.parse_args() 47 | 48 | 49 | if __name__ == '__main__': 50 | args = parse_args() 51 | if not os.path.exists(args.output_dir): 52 | os.makedirs(args.output_dir) 53 | main(args) 54 | -------------------------------------------------------------------------------- /facial_beauty_predictor/backbone/facenet.py: -------------------------------------------------------------------------------- 1 | """Functions for building the face recognition network. 2 | """ 3 | 4 | import os 5 | import re 6 | 7 | import tensorflow as tf 8 | from tensorflow.python.platform import gfile 9 | 10 | 11 | def load_model(model, input_map=None): 12 | # Check if the model is a model directory (containing a metagraph and a checkpoint file) 13 | # or if it is a protobuf file with a frozen graph 14 | model_exp = os.path.expanduser(model) 15 | if (os.path.isfile(model_exp)): 16 | with gfile.FastGFile(model_exp, 'rb') as f: 17 | graph_def = tf.GraphDef() 18 | graph_def.ParseFromString(f.read()) 19 | tf.import_graph_def(graph_def, input_map=input_map, name='') 20 | else: 21 | meta_file, ckpt_file = get_model_filenames(model_exp) 22 | saver = tf.compat.v1.train.import_meta_graph(os.path.join( 23 | model_exp, meta_file), input_map=input_map) 24 | saver.restore(tf.compat.v1.get_default_session(), 25 | os.path.join(model_exp, ckpt_file)) 26 | 27 | 28 | def get_model_filenames(model_dir): 29 | files = os.listdir(model_dir) 30 | meta_files = [s for s in files if s.endswith('.meta')] 31 | if len(meta_files) == 0: 32 | raise ValueError( 33 | 'No meta file found in the model directory (%s)' % model_dir) 34 | elif len(meta_files) > 1: 35 | raise ValueError( 36 | 'There should not be more than one meta file in the model directory (%s)' % model_dir) 37 | meta_file = meta_files[0] 38 | ckpt = tf.train.get_checkpoint_state(model_dir) 39 | if ckpt and ckpt.model_checkpoint_path: 40 | ckpt_file = os.path.basename(ckpt.model_checkpoint_path) 41 | return meta_file, ckpt_file 42 | 43 | meta_files = [s for s in files if '.ckpt' in s] 44 | max_step = -1 45 | for f in files: 46 | step_str = re.match(r'(^model-[\w\- ]+.ckpt-(\d+))', f) 47 | if step_str is not None and len(step_str.groups()) >= 2: 48 | step = int(step_str.groups()[1]) 49 | if step > max_step: 50 | max_step = step 51 | ckpt_file = step_str.groups()[0] 52 | return meta_file, ckpt_file 53 | -------------------------------------------------------------------------------- /scripts/generate_features_async.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | import pickle 5 | 6 | import numpy as np 7 | from scipy.special import logit 8 | 9 | sys.path.append(os.path.abspath('../facial_beauty_predictor')) # Ignore 10 | 11 | from facial_beauty_predictor.worker import worker_mtcnn_async, worker_mtcnn_facenet_async, worker_mtcnn_facenet_2_async 12 | 13 | 14 | def main(args): 15 | db_dict = pickle.load(open(args.db_file, "rb")) 16 | db = db_dict["all"] 17 | 18 | if args.backbone == "mtcnn-facenet": 19 | features_dict = worker_mtcnn_facenet_async( 20 | db, args.facenet_model_path, skip_multiple_faces=bool(args.skip_multiple_faces)) 21 | elif args.backbone == "mtcnn": 22 | features_dict = worker_mtcnn_async(db) 23 | elif args.backbone == "mtcnn-facenet-2": 24 | features_dict = worker_mtcnn_facenet_2_async( 25 | db, args.facenet_model_path) 26 | else: 27 | raise ValueError 28 | 29 | output_dir = os.path.join(args.output_dir, args.backbone) 30 | if not os.path.exists(output_dir): 31 | os.makedirs(output_dir) 32 | features_path = os.path.join(output_dir, "features.npy") 33 | print("Saving features to {}".format(features_path)) 34 | np.save(open(features_path, "wb"), features_dict) 35 | 36 | 37 | def parse_args(): 38 | parser = argparse.ArgumentParser() 39 | parser.add_argument( 40 | '--db', 41 | dest='db_file', 42 | type=str 43 | ) 44 | parser.add_argument( 45 | '--output-dir', 46 | dest='output_dir', 47 | default='data', 48 | type=str 49 | ) 50 | parser.add_argument( 51 | '--skip-multiple-faces', 52 | dest='skip_multiple_faces', 53 | action="store_true", 54 | ) 55 | parser.add_argument( 56 | '--facenet-model-path', 57 | dest='facenet_model_path', 58 | default="data/20170512-110547", 59 | type=str 60 | ) 61 | parser.add_argument( 62 | '--backbone', 63 | help="One of {mtcnn, mtcnn-facenet, mtcnn-facenet-2}", 64 | dest='backbone', 65 | default="mtcnn-facenet", 66 | type=str 67 | ) 68 | return parser.parse_args() 69 | 70 | 71 | if __name__ == '__main__': 72 | args = parse_args() 73 | if not os.path.exists(args.output_dir): 74 | os.makedirs(args.output_dir) 75 | main(args) 76 | -------------------------------------------------------------------------------- /scripts/convert_hotornot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import argparse 4 | import csv 5 | 6 | from config import SCORE_MARGIN 7 | 8 | 9 | # db_dict = {split: {profile_id: {img_paths, score}}} 10 | 11 | 12 | def main(args): 13 | db_list = [] 14 | for i in range(5): 15 | split = i + 1 16 | with open(os.path.join(args.db_dir, "eccv2010_split{}.csv".format(split))) as f: 17 | rows = list(csv.reader(f, delimiter=',')) 18 | scores = [float(row[1]) for row in rows] 19 | min_score = min(scores) - SCORE_MARGIN 20 | max_score = max(scores) + SCORE_MARGIN 21 | rows_train = [row for row in rows if row[2] == "train"] 22 | rows_test = [row for row in rows if row[2] == "test"] 23 | db_train = _rows_to_db(rows_train, min_score, max_score) 24 | db_test = _rows_to_db(rows_test, min_score, max_score) 25 | db_list.append((db_train, db_test)) 26 | db_dict = {"train{}".format(i + 1): db[0] for i, db in enumerate(db_list)} 27 | db_dict.update({"test{}".format(i + 1): db[1] 28 | for i, db in enumerate(db_list)}) 29 | db_dict_all = {**db_dict["train1"], **db_dict["test1"]} 30 | db_dict_female = {key: el for key, 31 | el in db_dict_all.items() if el["meta"]["gender"] == "F"} 32 | db_dict.update(dict( 33 | data=dict(all=db_dict_all, 34 | train=db_dict["train1"], 35 | test=db_dict["test1"], 36 | f=db_dict_female), 37 | meta=dict(genders=("F",), ethnicities=False) 38 | )) 39 | print("Stats:") 40 | print("Total size: {}".format(len(db_dict_all.keys()))) 41 | print("Female size: {}".format(len(db_dict_female.keys()))) 42 | pickle.dump(db_dict, open(os.path.join( 43 | args.output_dir, "hotornot.pkl"), "wb")) 44 | 45 | 46 | def _rows_to_db(rows, min_score, max_score): 47 | return {row[0][:-4]: { 48 | "img_paths": [os.path.join(args.db_dir, "hotornot_face", row[0][:-3] + "jpg")], 49 | "score": (float(row[1]) - min_score) / (max_score - min_score), 50 | "meta": dict(gender="F" if row[0][:6] == "female" else "M") 51 | } for row in rows} 52 | 53 | 54 | def parse_args(): 55 | parser = argparse.ArgumentParser() 56 | parser.add_argument( 57 | '--db-dir', 58 | dest='db_dir', 59 | type=str 60 | ) 61 | parser.add_argument( 62 | '--output-dir', 63 | dest='output_dir', 64 | default='data', 65 | type=str 66 | ) 67 | return parser.parse_args() 68 | 69 | 70 | if __name__ == '__main__': 71 | args = parse_args() 72 | if not os.path.exists(args.output_dir): 73 | os.makedirs(args.output_dir) 74 | main(args) 75 | -------------------------------------------------------------------------------- /scripts/infer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import pickle 4 | 5 | import numpy as np 6 | import imageio 7 | import joblib 8 | from scipy.special import expit 9 | import matplotlib.pyplot as plt 10 | 11 | 12 | def main(args): 13 | db_dict = pickle.load(open(args.db_file, "rb")) 14 | db = db_dict["all"] 15 | 16 | features = np.load(open(args.features, "rb"), allow_pickle=True).item() 17 | features = [features[profile_id] for profile_id in db.keys()] 18 | nan_idx = np.argwhere(np.isnan(features)) 19 | nan_idx = np.unique(nan_idx[:, 0]) 20 | print("Found {}/{} images without faces".format(len(nan_idx), len(db))) 21 | features = np.delete(features, nan_idx, axis=0) 22 | db_flat = np.delete(list(db.values()), nan_idx) 23 | 24 | model = joblib.load(args.model_file) 25 | logits_pred = model.predict(features) 26 | scores_pred = np.array([expit(y) for y in logits_pred]) * 10 27 | 28 | print("Score mean: {:.1f}".format(np.mean(scores_pred))) 29 | print("Score median: {:.1f}".format(np.median(scores_pred))) 30 | print("Score standard deviation: {:.1f}".format(np.std(scores_pred))) 31 | 32 | sorted_scores = sorted(scores_pred) 33 | plt.plot(np.arange(0, 1, 1 / len(sorted_scores)), sorted_scores) 34 | plt.grid(linestyle="--") 35 | plt.xlabel("prob to have a score between 0 and y") 36 | plt.ylabel("score") 37 | plt.show() 38 | 39 | # visualize some random profiles 40 | for i in range(len(db_flat)): 41 | random_idx = np.random.choice(range(len(db_flat))) 42 | # random_idx = i 43 | img_paths = db_flat[random_idx]["img_paths"] 44 | score = scores_pred[random_idx] 45 | # if score > 4: 46 | # continue 47 | 48 | img_paths = [ 49 | img_path for img_path in img_paths if os.path.exists(img_path)] 50 | if not len(img_paths): 51 | continue 52 | 53 | img_list = [] 54 | for img_path in img_paths: 55 | img = imageio.imread(img_path) 56 | img_list.append(img) 57 | plt.figure(figsize=(14, 8)) 58 | fig = plt.imshow(np.vstack(img_list)) 59 | plt.title("score {:.1f}".format(score)) 60 | fig.axes.get_xaxis().set_visible(False) 61 | fig.axes.get_yaxis().set_visible(False) 62 | 63 | plt.show() 64 | 65 | 66 | def parse_args(): 67 | parser = argparse.ArgumentParser() 68 | parser.add_argument( 69 | '--db', 70 | dest='db_file', 71 | type=str 72 | ) 73 | parser.add_argument( 74 | '--output-dir', 75 | dest='output_dir', 76 | default='data/tinder', 77 | type=str 78 | ) 79 | parser.add_argument( 80 | '--model', 81 | dest='model_file', 82 | type=str 83 | ) 84 | parser.add_argument( 85 | '--features', 86 | dest='features', 87 | type=str 88 | ) 89 | return parser.parse_args() 90 | 91 | 92 | if __name__ == '__main__': 93 | args = parse_args() 94 | main(args) 95 | -------------------------------------------------------------------------------- /facial_beauty_predictor/backbone/mtcnn/network.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | 3 | 4 | class Network(object): 5 | 6 | def __init__(self, session, trainable: bool = True): 7 | """ 8 | Initializes the network. 9 | :param trainable: flag to determine if this network should be trainable or not. 10 | """ 11 | self._session = session 12 | self.__trainable = trainable 13 | self.__layers = {} 14 | self.__last_layer_name = None 15 | 16 | with tf.compat.v1.variable_scope(self.__class__.__name__.lower()): 17 | self._config() 18 | 19 | def _config(self): 20 | """ 21 | Configures the network layers. 22 | It is usually done using the LayerFactory() class. 23 | """ 24 | raise NotImplementedError( 25 | "This method must be implemented by the network.") 26 | 27 | def add_layer(self, name: str, layer_output): 28 | """ 29 | Adds a layer to the network. 30 | :param name: name of the layer to add 31 | :param layer_output: output layer. 32 | """ 33 | self.__layers[name] = layer_output 34 | self.__last_layer_name = name 35 | 36 | def get_layer(self, name: str = None): 37 | """ 38 | Retrieves the layer by its name. 39 | :param name: name of the layer to retrieve. If name is None, it will retrieve the last added layer to the 40 | network. 41 | :return: layer output 42 | """ 43 | if name is None: 44 | name = self.__last_layer_name 45 | 46 | return self.__layers[name] 47 | 48 | def is_trainable(self): 49 | """ 50 | Getter for the trainable flag. 51 | """ 52 | return self.__trainable 53 | 54 | def set_weights(self, weights_values: dict, ignore_missing=False): 55 | """ 56 | Sets the weights values of the network. 57 | :param weights_values: dictionary with weights for each layer 58 | """ 59 | network_name = self.__class__.__name__.lower() 60 | 61 | with tf.compat.v1.variable_scope(network_name): 62 | for layer_name in weights_values: 63 | with tf.compat.v1.variable_scope(layer_name, reuse=True): 64 | for param_name, data in weights_values[layer_name].items(): 65 | try: 66 | var = tf.compat.v1.get_variable( 67 | param_name, use_resource=False) 68 | self._session.run(var.assign(data)) 69 | 70 | except ValueError: 71 | if not ignore_missing: 72 | raise 73 | 74 | def feed(self, image): 75 | """ 76 | Feeds the network with an image 77 | :param image: image (perhaps loaded with CV2) 78 | :return: network result 79 | """ 80 | network_name = self.__class__.__name__.lower() 81 | 82 | with tf.compat.v1.variable_scope(network_name): 83 | return self._feed(image) 84 | 85 | def _feed(self, image): 86 | raise NotImplementedError("Method not implemented.") 87 | -------------------------------------------------------------------------------- /scripts/scut_cum_stats.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pickle 4 | import argparse 5 | import logging 6 | 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | from scipy.special import logit 10 | 11 | from utils import normalize_dataset 12 | 13 | 14 | def main(args): 15 | db_dict = pickle.load(open(args.db_file, "rb")) 16 | db_dict = normalize_dataset(db_dict)["data"] 17 | 18 | sorted_scores = sorted([d["score"] for d in db_dict["all"].values()]) 19 | x = np.linspace(0, 1, len(sorted_scores)) * 100 20 | 21 | _, ax = plt.subplots(figsize=(14, 8)) 22 | ax.plot(x, sorted_scores, label=args.db_file) 23 | 24 | mean_scores = sorted_scores 25 | x_mean = x 26 | 27 | if args.db_extra_file: 28 | db_extra_dict = pickle.load(open(args.db_extra_file, "rb")) 29 | db_extra_dict = normalize_dataset(db_extra_dict)["data"] 30 | sorted_scores_extra = sorted([d["score"] 31 | for d in db_extra_dict["all"].values()]) 32 | x_extra = np.linspace(0, 1, len(sorted_scores_extra)) * 100 33 | ax.plot(x_extra, sorted_scores_extra, label=args.db_extra_file) 34 | 35 | if len(sorted_scores_extra) > len(sorted_scores): 36 | sorted_scores_extra = np.interp(x, x_extra, sorted_scores_extra) 37 | else: 38 | sorted_scores = np.interp(x_extra, x, sorted_scores) 39 | x_mean = x_extra 40 | 41 | mean_scores = (sorted_scores + sorted_scores_extra) / 2 42 | 43 | ax.plot(x_mean, mean_scores, label="mean") 44 | ax.legend() 45 | # ax.plot(x, [logit(y) for y in sorted_scores]) 46 | ax.grid(linestyle="--") 47 | plt.xlabel("prob to have a score between 0 and y [%]") 48 | plt.ylabel("score") 49 | plt.show() 50 | 51 | percentiles = np.interp(np.linspace(0, 1, 20), x_mean / 100, mean_scores) 52 | for i, percentil in enumerate(percentiles): 53 | print("Top {}% have a score higher than {:.3f}".format( 54 | 100 - (i * 5), percentil)) 55 | 56 | np.save(open("data/percentiles.npy", "wb"), percentiles) 57 | 58 | # # fit polynomial to mean scores 59 | # ORDER = 3 60 | # coeffs = np.polyfit(x_mean, mean_scores, ORDER) 61 | # approx_scores = 0 62 | # for i, coeff in enumerate(reversed(coeffs)): 63 | # approx_scores += coeff * x_mean ** i 64 | # plt.figure() 65 | # plt.plot(x_mean, mean_scores, label="mean") 66 | # plt.plot(x_mean, approx_scores, label="approx") 67 | # plt.legend() 68 | # plt.grid(linestyle="--") 69 | # plt.xlabel("prob to have a score between 0 and y") 70 | # plt.ylabel("score") 71 | # plt.show() 72 | 73 | 74 | def parse_args(): 75 | parser = argparse.ArgumentParser() 76 | parser.add_argument( 77 | '--db', 78 | dest='db_file', 79 | type=str 80 | ) 81 | parser.add_argument( 82 | '--db-extra', 83 | dest='db_extra_file', 84 | type=str 85 | ) 86 | if len(sys.argv) == 1: 87 | parser.print_help() 88 | sys.exit(1) 89 | return parser.parse_args() 90 | 91 | 92 | if __name__ == '__main__': 93 | args = parse_args() 94 | logging.info('Called with args:') 95 | logging.info(args) 96 | main(args) 97 | -------------------------------------------------------------------------------- /scripts/convert_scut.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import argparse 4 | 5 | 6 | from config import SCORE_MARGIN 7 | 8 | 9 | # db_dict = {split: {profile_id: {img_paths, score}}} 10 | 11 | 12 | def main(args): 13 | db_list = [] 14 | for path in ("All_labels.txt", "split_of_60%training and 40%testing/train.txt", "split_of_60%training and 40%testing/test.txt"): 15 | with open(os.path.join(args.db_dir, "train_test_files", path)) as f: 16 | lines = f.read().splitlines() 17 | rows = [line.split(" ") for line in lines] 18 | scores = [float(row[1]) for row in rows] 19 | min_score = min(scores) - SCORE_MARGIN 20 | max_score = max(scores) + SCORE_MARGIN 21 | db = {row[0][:-4]: { 22 | "img_paths": [os.path.join(args.db_dir, "Images", row[0])], 23 | # normalize [0, 1] 24 | "score": (float(row[1]) - min_score) / (max_score - min_score), 25 | "meta": dict(gender=row[0][1], ethnicity=row[0][0]) 26 | } for row in rows} 27 | db_list.append(db) 28 | db_dict_all = db_list[0] 29 | db_dict_female = {key: el for key, 30 | el in db_dict_all.items() if el["meta"]["gender"] == "F"} 31 | db_dict_male = {key: el for key, 32 | el in db_dict_all.items() if el["meta"]["gender"] == "M"} 33 | db_dict_asian_female = {key: el for key, 34 | el in db_dict_female.items() if el["meta"]["ethnicity"] == "A"} 35 | db_dict_caucasian_female = {key: el for key, 36 | el in db_dict_female.items() if el["meta"]["ethnicity"] == "C"} 37 | db_dict_asian_male = {key: el for key, 38 | el in db_dict_male.items() if el["meta"]["ethnicity"] == "A"} 39 | db_dict_caucasian_male = {key: el for key, 40 | el in db_dict_male.items() if el["meta"]["ethnicity"] == "C"} 41 | db_dict = dict(data=dict(all=db_dict_all, train=db_list[1], test=db_list[2], 42 | f=db_dict_female, 43 | m=db_dict_male, 44 | af=db_dict_asian_female, 45 | am=db_dict_asian_male, 46 | cf=db_dict_caucasian_female, 47 | cm=db_dict_caucasian_male), 48 | meta=dict(genders=("F", "M"), ethnicities=["A", "C"])) 49 | print("Stats:") 50 | print("Total size: {}".format(len(db_dict_all.keys()))) 51 | print("Female size: {}".format(len(db_dict_female.keys()))) 52 | print("Male size: {}".format(len(db_dict_male.keys()))) 53 | print("Asian female size: {}".format(len(db_dict_asian_female.keys()))) 54 | print("Asian male size: {}".format(len(db_dict_asian_male.keys()))) 55 | print("Caucasian female size: {}".format( 56 | len(db_dict_caucasian_female.keys()))) 57 | print("Caucasian male size: {}".format(len(db_dict_caucasian_male.keys()))) 58 | pickle.dump(db_dict, open(os.path.join( 59 | args.output_dir, "scut.pkl"), "wb")) 60 | 61 | 62 | def parse_args(): 63 | parser = argparse.ArgumentParser() 64 | parser.add_argument( 65 | '--db-dir', 66 | dest='db_dir', 67 | type=str 68 | ) 69 | parser.add_argument( 70 | '--output-dir', 71 | dest='output_dir', 72 | default='data', 73 | type=str 74 | ) 75 | return parser.parse_args() 76 | 77 | 78 | if __name__ == '__main__': 79 | args = parse_args() 80 | if not os.path.exists(args.output_dir): 81 | os.makedirs(args.output_dir) 82 | main(args) 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Facial Beauty Predictor 2 | 3 | A deep learning model based on FaceNet and MTCNN to predict a beauty score for faces in images. The CNN outperformes the state-of-the art by up to 18% (2019). 4 | 5 | Included are scripts for generating features from images, training regressors, as well as a async server for inference based on gunicorn / gevent. 6 | 7 | ## Based on 8 | 9 | [tindetheus](https://github.com/cjekel/tindetheus) 10 | 11 | [FaceNet](https://github.com/davidsandberg/facenet) 12 | 13 | [MTCNN](https://github.com/ipazc/mtcnn) 14 | 15 | ## Requirements: 16 | 17 | - python 3.7 18 | - [pipenv](https://github.com/pypa/pipenv) 19 | 20 | ## Installation 21 | 22 | `pipenv install --dev` 23 | 24 | ## Quick Start 25 | 26 | - Download [SCUT](https://github.com/HCIILAB/SCUT-FBP5500-Database-Release) dataset 27 | - Download [HotOrNot](https://www.researchgate.net/publication/261595808_Female_Facial_Beauty_Dataset_ECCV2010_v10) dataset 28 | - Download the FaceNet model [20170512-110547](https://drive.google.com/file/d/0B5MzpY9kBtDVZ2RpVDYwWmxoSUk/edit) and extract it into the `data` directory 29 | - Convert datasets with: 30 | - `python scripts/convert_scut.py --db-dir ` 31 | - `python scripts/convert_tinder.py --db-dir ` 32 | - `python scripts/convert_hotornot.py --db-dir ` 33 | - Generate features for SCUT dataset once and store them to the disk: 34 | - `python scripts/generate_features_async.py --db data/scut.pkl --output-dir data/scut` 35 | - Train regressor models with 36 | - `python scripts/train_regressor.py --db data/scut.pkl --output-dir data/scut/mtcnn-facenet --features data/scut/mtcnn-facenet/features.npy` 37 | - Compare regressors: 38 | - `python scripts/compare_models.py --db data/scut.pkl --output-dir data/scut/mtcnn-facenet --models-dir data/scut/mtcnn-facenet/models --features data/scut/mtcnn-facenet/features.npy` 39 | - Generate model trained on all the dataset: 40 | - `python scripts/train_regressor.py --db data/scut.pkl --output-dir data/scut/mtcnn-facenet --features data/scut/mtcnn-facenet/features.npy --no-split` 41 | - Generate features for Tinder dataset once and store them to the disk: 42 | - `python scripts/generate_features_async.py --db data/tinder.pkl --output-dir data/tinder` 43 | - Infer results on tinder dataset: 44 | - `python scripts/infer.py --db data/tinder.pkl --features data/tinder/mtcnn-facenet/features.npy --model data/scut/mtcnn-facenet/models/all/sklearn.linear_model.base.LinearRegression_1.pkl` 45 | 46 | Those steps can be repeated for a mtcnn-only backbone (put `--backbone mtcnn` flag where necessary and replace `mtcnn-facenet` with `mtcnn`) 47 | 48 | ## Results 49 | 50 | ### SCUT Dataset 51 | 52 | #### FaceNet features 53 | 54 | | Regressor | PC | 55 | | --------- | ----- | 56 | | Lasso | 0.846 | 57 | | Ridge | 0.872 | 58 | | Linear | 0.872 | 59 | 60 | #### FaceNet + MTCNN features: 61 | 62 | | Regressor | PC | 63 | | --------- | --- | 64 | 65 | 66 | @TODO (note: was slightly better than Facenet features only) 67 | 68 | #### MTCNN only features 69 | 70 | | Regressor | PC | 71 | | --------- | --- | 72 | 73 | 74 | @TODO 75 | 76 | ### HotOrNot Dataset 77 | 78 | #### FaceNet features 79 | 80 | | Regressor | PC | 81 | | --------- | ----- | 82 | | Linear | 0.536 | 83 | | Lasso | 0.550 | 84 | | Ridge | 0.567 | 85 | 86 | #### FaceNet + MTCNN features 87 | 88 | | Regressor | PC | 89 | | --------- | --- | 90 | 91 | 92 | @TODO 93 | 94 | #### MTCNN only features 95 | 96 | | Regressor | PC | 97 | | --------- | --- | 98 | 99 | 100 | @TODO 101 | -------------------------------------------------------------------------------- /facial_beauty_predictor/server/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import logging 4 | from requests.exceptions import RequestException 5 | from queue import SimpleQueue 6 | import uuid 7 | import logging 8 | from concurrent.futures import ThreadPoolExecutor 9 | import time 10 | 11 | from flask import Flask, request 12 | from flask_json import FlaskJSON, json_response, JsonError 13 | import sentry_sdk 14 | from sentry_sdk.integrations.flask import FlaskIntegration 15 | import numpy as np 16 | 17 | from facial_beauty_predictor.server.init_app import fetch_files, init_worker 18 | from facial_beauty_predictor.server.utils.env import is_main_run 19 | 20 | 21 | DEV = os.environ.get("FLASK_ENV") == "development" 22 | logging.basicConfig(level=logging.DEBUG if DEV else logging.INFO) 23 | logging.getLogger("tensorflow").setLevel(logging.ERROR) 24 | logging.getLogger("PIL").setLevel(logging.INFO) 25 | logging.getLogger("s3transfer").setLevel(logging.INFO) 26 | logging.getLogger("botocore").setLevel(logging.INFO) 27 | logging.getLogger("urllib3").setLevel(logging.INFO) 28 | # logging.getLogger("werkzeug").setLevel(logging.INFO) 29 | # logging.getLogger("metric").setLevel(logging.INFO) 30 | 31 | 32 | def create_app(): 33 | start = time.time() 34 | 35 | main_run = is_main_run() 36 | 37 | # create and configure the app 38 | app = Flask(__name__) 39 | app.config.from_object(os.environ.get( 40 | 'APP_SETTINGS', "facial_beauty_predictor.server.config.Config")) 41 | 42 | if main_run: 43 | logging.info("Creating app with config:\n" + 44 | '\n'.join(" {}: {}".format(k, v) 45 | for k, v in app.config.items())) 46 | 47 | if main_run: 48 | app.errorhandler(Exception)(_on_exception) 49 | 50 | # if app.config["LOG_METRICS"]: 51 | # metrics.init_app(app) 52 | # request_check.init_app(app) 53 | 54 | fetch_files(app) 55 | app.config["PERCENTILE_QUEUES"] = {} 56 | img_paths_queue = init_worker(app) 57 | app.config["IMG_PATHS_QUEUE"] = img_paths_queue 58 | 59 | json = FlaskJSON() 60 | json.init_app(app) 61 | 62 | init_app(app) 63 | 64 | if not app.config["DEBUG"]: 65 | sentry_sdk.init( 66 | dsn="https://af0dce792f934abaac5bb9d7bcad7df8@sentry.io/1785265", 67 | integrations=[FlaskIntegration()], 68 | ) 69 | 70 | logging.info("App initialization done. Took {0:.1f}s".format( 71 | time.time() - start)) 72 | 73 | return app 74 | 75 | 76 | def _on_exception(e): 77 | logging.exception(type(e).__name__) 78 | return json_response(500) 79 | 80 | 81 | def init_app(app): 82 | @app.route('/', methods=["POST"]) 83 | def estimate_score(): 84 | start = time.time() 85 | paths = request.json.get('paths') 86 | task_id = uuid.uuid4() 87 | 88 | assert len(paths) 89 | 90 | result_queue = SimpleQueue() 91 | app.config["PERCENTILE_QUEUES"][task_id] = result_queue 92 | img_paths_queue = app.config["IMG_PATHS_QUEUE"] 93 | img_paths_queue.put(dict(paths=paths, id=task_id)) 94 | percentile = result_queue.get() 95 | if percentile is None: 96 | raise JsonError(500) 97 | data = dict(percentile=percentile) 98 | logging.info("Percentile: {:.0f}. Estimation took {:.3f}s".format( 99 | percentile * 100, time.time() - start)) 100 | return json_response(data=data) 101 | 102 | @app.route('/ping', methods=["GET"]) 103 | def ping(): 104 | return "", 200 105 | -------------------------------------------------------------------------------- /facial_beauty_predictor/server/init_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from queue import Queue 3 | from threading import Thread 4 | import joblib 5 | from urllib.parse import urlparse 6 | import urllib.request 7 | import logging 8 | 9 | import boto3 10 | import numpy as np 11 | 12 | from facial_beauty_predictor.worker import worker_mtcnn_facenet_async_queue 13 | 14 | 15 | def fetch_files(app): 16 | if is_remote(app.config["FACENET_MODEL_PATH"]): 17 | local_facenet_model_path = "data/facenet" 18 | if not folder_exists(local_facenet_model_path): 19 | download_s3_folder( 20 | app.config["FACENET_MODEL_PATH"], local_facenet_model_path) 21 | logging.debug("Downloaded facenet model to {}".format( 22 | local_facenet_model_path)) 23 | app.config["FACENET_MODEL_PATH"] = local_facenet_model_path 24 | if is_remote(app.config["REGRESSOR_MODEL_PATH"]): 25 | local_regressor_model_path = "data/regressor_model.pkl" 26 | if not file_exists(local_regressor_model_path): 27 | download_s3_file( 28 | app.config["REGRESSOR_MODEL_PATH"], local_regressor_model_path) 29 | logging.debug("Downloaded regressor model to {}".format( 30 | local_regressor_model_path)) 31 | app.config["REGRESSOR_MODEL_PATH"] = local_regressor_model_path 32 | if is_remote(app.config["PERCENTILS_PATH"]): 33 | local_percentiles_path = "data/percentiles.npy" 34 | if not file_exists(local_percentiles_path): 35 | download_s3_file( 36 | app.config["PERCENTILS_PATH"], local_percentiles_path) 37 | logging.debug("Downloaded percentiles to {}".format( 38 | local_percentiles_path)) 39 | app.config["PERCENTILS_PATH"] = local_percentiles_path 40 | 41 | 42 | def init_worker(app): 43 | img_paths_queue = Queue() 44 | regressor_model = joblib.load(app.config["REGRESSOR_MODEL_PATH"]) 45 | percentiles = np.load(open(app.config["PERCENTILS_PATH"], "rb")) 46 | results_queue = worker_mtcnn_facenet_async_queue( 47 | img_paths_queue, app.config["FACENET_MODEL_PATH"], regressor_model, percentiles) 48 | result_queues = app.config["PERCENTILE_QUEUES"] 49 | thread = Thread(target=distribute_results, 50 | args=(results_queue, result_queues,), name="DistributeResultsThread", daemon=True) 51 | thread.start() 52 | return img_paths_queue 53 | 54 | 55 | def distribute_results(results_queue, result_queues): 56 | while True: 57 | result = results_queue.get() 58 | result_queue = result_queues[result["id"]] 59 | result_queue.put(result["percentile"]) 60 | 61 | 62 | def is_remote(url): 63 | return bool(urlparse(url).netloc) 64 | 65 | 66 | def file_exists(path): 67 | return os.path.exists(path) and os.path.isfile(path) 68 | 69 | 70 | def folder_exists(path): 71 | return os.path.exists(path) and os.path.isdir(path) 72 | 73 | 74 | def download_s3_folder(path, dest): 75 | path = path[5:] 76 | bucket_name = path.split("/")[0] 77 | s3_resource = boto3.resource("s3") 78 | bucket = s3_resource.Bucket(bucket_name) 79 | remote_directory_name = "/".join(path.split("/")[1:]) 80 | for obj in bucket.objects.filter(Prefix=remote_directory_name): 81 | file_name = obj.key[len(remote_directory_name) + 1:] 82 | dest_folder = os.path.join( 83 | dest, os.path.dirname(file_name)) 84 | if not os.path.exists(dest_folder): 85 | os.makedirs(dest_folder) 86 | bucket.download_file(obj.key, os.path.join( 87 | dest, file_name)) 88 | 89 | 90 | def download_s3_file(path, dest): 91 | path = path[5:] 92 | bucket_name = path.split("/")[0] 93 | s3_resource = boto3.resource("s3") 94 | bucket = s3_resource.Bucket(bucket_name) 95 | remote_file_name = "/".join(path.split("/")[1:]) 96 | bucket.download_file(remote_file_name, dest) 97 | -------------------------------------------------------------------------------- /scripts/compare_models.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import os 4 | import importlib 5 | import pickle 6 | import joblib 7 | import math 8 | 9 | import sklearn.metrics as metrics 10 | from scipy.stats import pearsonr 11 | import numpy as np 12 | import matplotlib.pyplot as plt 13 | 14 | 15 | def main(args): 16 | model_files = sorted([model_file for model_file in os.listdir( 17 | args.models_dir) if model_file[0] != "." and os.path.isfile(os.path.join(args.models_dir, model_file))]) 18 | 19 | db_dict = pickle.load(open(args.db_file, "rb")) 20 | db = db_dict["test"] 21 | features = np.load(open(args.features, "rb"), allow_pickle=True).item() 22 | features = [features[profile_id] for profile_id in db.keys()] 23 | nan_idx = np.argwhere(np.isnan(features)) 24 | nan_idx = np.unique(nan_idx[:, 0]) 25 | print("Found {} images without faces in test split".format(len(nan_idx))) 26 | features = np.delete(features, nan_idx, axis=0) 27 | db_flat = np.delete(list(db.values()), nan_idx) 28 | 29 | scores = np.array([d["score"] for d in db_flat]) 30 | 31 | sorted_inds = np.argsort(scores) 32 | scores_sorted = scores[sorted_inds] 33 | 34 | x = np.arange(0, 1, 1 / len(features)) 35 | 36 | n_plots = len(model_files) 37 | n_cols = 3 # min(math.ceil(np.sqrt(n_plots)), 5) 38 | n_rows = max(math.ceil(n_plots / n_cols), 2) 39 | 40 | fig, axs = plt.subplots(n_rows, n_cols, figsize=(14, 8)) 41 | fig.suptitle('Model comparison') 42 | 43 | FONTSIZE = 8 44 | plt.rcParams.update({'font.size': FONTSIZE}) 45 | 46 | for i, model_file in enumerate(model_files): 47 | row = int(i / n_cols) 48 | col = i - row * n_cols 49 | ax = axs[row, col] 50 | 51 | model_name = model_file[:-6].split(".")[-1] 52 | 53 | model = joblib.load(os.path.join(args.models_dir, model_file)) 54 | 55 | order = model.steps[0][1].degree 56 | alpha = None 57 | if hasattr(model.steps[-1][1], "alpha"): 58 | alpha = model.steps[-1][1].alpha 59 | 60 | scores_pred = model.predict(features) 61 | scores_pred = np.clip(scores_pred, 0, 1) 62 | pc = pearsonr(scores_pred, scores) 63 | 64 | ax.plot(scores_sorted, 65 | scores_pred[sorted_inds], ".", label="pred", markersize=2) 66 | ax.plot(scores_sorted, scores_sorted, label="GT") 67 | ax.legend() 68 | ax.set_ylabel('score', fontsize=FONTSIZE) 69 | ax.set_xlabel('score GT', fontsize=FONTSIZE) 70 | title_str = "{} (order: {}".format(model_name, order) 71 | if alpha is not None: 72 | title_str += ", alpha: {:.2E})".format(alpha) 73 | else: 74 | title_str += ")" 75 | title_str += "\nPC {:.3f}".format(pc[0]) 76 | ax.set_title(title_str) 77 | ax.grid(linestyle='--') 78 | for tick in ax.xaxis.get_major_ticks() + ax.yaxis.get_major_ticks(): 79 | tick.label.set_fontsize(FONTSIZE) 80 | plt.tight_layout(rect=[0, 0.03, 1, 0.95]) 81 | plt.savefig(os.path.join(args.output_dir, "model_comparison.png")) 82 | plt.show() 83 | 84 | 85 | def parse_args(): 86 | parser = argparse.ArgumentParser() 87 | parser.add_argument( 88 | '--db', 89 | dest='db_file', 90 | default=None, 91 | type=str 92 | ) 93 | parser.add_argument( 94 | '--output-dir', 95 | dest='output_dir', 96 | default='data', 97 | type=str 98 | ) 99 | parser.add_argument( 100 | '--models-dir', 101 | dest='models_dir', 102 | default='data/models', 103 | type=str 104 | ) 105 | parser.add_argument( 106 | '--features', 107 | dest='features', 108 | default='data/features.npy', 109 | type=str 110 | ) 111 | return parser.parse_args() 112 | 113 | 114 | if __name__ == '__main__': 115 | args = parse_args() 116 | if not os.path.exists(args.output_dir): 117 | os.makedirs(args.output_dir) 118 | main(args) 119 | -------------------------------------------------------------------------------- /scripts/eval_hotornot.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import importlib 4 | import pickle 5 | import joblib 6 | import math 7 | 8 | from scipy.optimize import minimize 9 | from scipy.special import expit, logit 10 | import sklearn.metrics as metrics 11 | from scipy.stats import pearsonr 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | import sklearn.linear_model as linear_model 15 | import sklearn.svm as svm 16 | import sklearn.ensemble as ensemble 17 | import sklearn.kernel_ridge as kernel_ridge 18 | import sklearn.isotonic as isotonic 19 | import sklearn.gaussian_process as gaussian_process 20 | from sklearn.preprocessing import PolynomialFeatures 21 | from sklearn.pipeline import Pipeline 22 | from sklearn.decomposition import PCA 23 | import sklearn.metrics as metrics 24 | from scipy.stats import pearsonr 25 | 26 | 27 | # uncomment regressors to train 28 | model_info_list = ( 29 | (linear_model.LinearRegression,), 30 | (linear_model.Lasso, dict(type="CONT", init=dict(alpha=1e-2))), 31 | (linear_model.Ridge, dict(type="CONT", init=dict(alpha=1))), 32 | (linear_model.BayesianRidge, dict(type="CONT", init=dict( 33 | alpha_1=1e-06, alpha_2=1e-06, lambda_1=1e-06, lambda_2=1e-06))), 34 | # (linear_model.SGDRegressor,), 35 | # (linear_model.ElasticNet,), # const 36 | # (linear_model.ARDRegression,), 37 | # (linear_model.HuberRegressor,), 38 | # (linear_model.Lars,), 39 | # (linear_model.LassoLars,), 40 | # (linear_model.PassiveAggressiveRegressor,), 41 | # (linear_model.TheilSenRegressor,), 42 | # (kernel_ridge.KernelRidge, dict(type="CONT", init=dict( 43 | # alpha=1), kwargs=dict(kernel='sigmoid'))), 44 | # (svm.SVR,), 45 | # (ensemble.AdaBoostRegressor,), 46 | # (ensemble.GradientBoostingRegressor,), 47 | # (ensemble.RandomForestRegressor,), 48 | # (gaussian_process.GaussianProcessRegressor, 49 | # dict(type="CONT", init=dict(alpha=1e-10))), 50 | ) 51 | 52 | 53 | def main(args): 54 | db_dict = pickle.load(open(args.db_file, "rb")) 55 | 56 | results_dict = {model_info[0].__name__: dict( 57 | RMSE=[], MAE=[], PC=[]) for model_info in model_info_list} 58 | 59 | for i in range(5): 60 | split = 1 + i 61 | db_train = db_dict["train{}".format(split)] 62 | db_test = db_dict["test{}".format(split)] 63 | 64 | features = np.load(open(args.features, "rb"), allow_pickle=True).item() 65 | features_train = [features[profile_id] 66 | for profile_id in db_train.keys()] 67 | features_test = [features[profile_id] for profile_id in db_test.keys()] 68 | nan_idx_train = np.argwhere(np.isnan(features_train)) 69 | nan_idx_test = np.argwhere(np.isnan(features_test)) 70 | nan_idx_train = np.unique(nan_idx_train[:, 0]) 71 | nan_idx_test = np.unique(nan_idx_test[:, 0]) 72 | print("Found {}/{} images without faces in train split".format(len(nan_idx_train), len(db_train))) 73 | print("Found {}/{} images without faces in test split".format(len(nan_idx_test), len(db_test))) 74 | features_train = np.delete(features_train, nan_idx_train, axis=0) 75 | features_test = np.delete(features_test, nan_idx_test, axis=0) 76 | db_flat_train = np.delete(list(db_train.values()), nan_idx_train) 77 | db_flat_test = np.delete(list(db_test.values()), nan_idx_test) 78 | 79 | scores_train = [d["score"] for d in db_flat_train] 80 | scores_test = [d["score"] for d in db_flat_test] 81 | 82 | logits_train = [logit(s) for s in scores_train] 83 | logits_test = [logit(s) for s in scores_test] 84 | 85 | results = [] 86 | for model_info in model_info_list: 87 | model_class = model_info[0] 88 | print("-" * 50) 89 | for i in range(args.max_order): 90 | order = i + 1 91 | print( 92 | "Fitting {} w/ features of order {}".format(model_class.__name__, order)) 93 | 94 | def get_model(kwargs={}): 95 | kwargs.update(meta.get("kwargs", {})) 96 | poly = PolynomialFeatures(order) 97 | model = model_class(*meta.get("args", {}), 98 | **kwargs) 99 | if args.pca: 100 | pca = PCA(n_components=args.pca) 101 | pipeline_list = [ 102 | ('poly', poly), ('pca', pca), ('fit', model)] 103 | else: 104 | pipeline_list = [('poly', poly), ('fit', model)] 105 | return Pipeline(pipeline_list) 106 | 107 | if len(model_info) == 2: 108 | meta = model_info[1] 109 | else: 110 | meta = {} 111 | if meta.get("type") is None: 112 | print("Constant params") 113 | model = get_model() 114 | model.fit(features_train, logits_train) 115 | elif meta["type"] == "GRID": 116 | print("Finding optimal params from grid") 117 | param_grid = {"fit__" + k: v for k, 118 | v in meta["grid"].items()} 119 | model = get_model() 120 | model = GridSearchCV(model, param_grid=param_grid,).fit( 121 | features_train, logits_train).best_estimator_ 122 | print(model) 123 | elif meta["type"] == "CONT": 124 | print("Optimizing continuous params") 125 | init = meta["init"] 126 | 127 | def func(x): 128 | kwargs = {k: x[i] for i, k in enumerate(init.keys())} 129 | model = get_model(kwargs) 130 | model.fit(features_train, logits_train) 131 | logits_pred = model.predict(features_train) 132 | mse = metrics.mean_squared_error( 133 | logits_pred, logits_train) 134 | return mse 135 | res = minimize(func, list(init.values()), 136 | method='Nelder-Mead') 137 | print(res) 138 | res_kwargs = {k: res.x[i] 139 | for i, k in enumerate(init.keys())} 140 | model = model_class(**res_kwargs) 141 | model.fit(features_train, logits_train) 142 | else: 143 | raise ValueError 144 | 145 | logits_pred = model.predict(features_test) 146 | scores_pred = np.array([expit(y) for y in logits_pred]) 147 | rmse = np.sqrt(metrics.mean_squared_error( 148 | scores_pred, scores_test)) 149 | mae = metrics.mean_absolute_error(scores_pred, scores_test) 150 | pc = pearsonr(scores_pred, scores_test)[0] 151 | 152 | results_dict[model_class.__name__]["RMSE"].append(rmse) 153 | results_dict[model_class.__name__]["MAE"].append(mae) 154 | results_dict[model_class.__name__]["PC"].append(pc) 155 | print(results_dict) 156 | for model_name, results in results_dict.items(): 157 | print("{}:\n".format(model_name) + ", ".join(["{}: {:.3f}".format(metric, np.mean(result)) 158 | for metric, result in results.items()])) 159 | 160 | 161 | def parse_args(): 162 | parser = argparse.ArgumentParser() 163 | parser.add_argument( 164 | '--db', 165 | dest='db_file', 166 | default='data/hotornot.pkl', 167 | type=str 168 | ) 169 | parser.add_argument( 170 | '--output-dir', 171 | dest='output_dir', 172 | default='data/hotornot/mtcnn-facenet', 173 | type=str 174 | ) 175 | parser.add_argument( 176 | '--features', 177 | dest='features', 178 | default='data/hotornot/mtcnn-facenet/features.npy', 179 | type=str 180 | ) 181 | parser.add_argument( 182 | '--pca', 183 | dest='pca', 184 | default=0, 185 | type=int 186 | ) 187 | parser.add_argument( 188 | '--max-order', 189 | dest='max_order', 190 | default=1, 191 | type=int 192 | ) 193 | return parser.parse_args() 194 | 195 | 196 | if __name__ == '__main__': 197 | args = parse_args() 198 | if not os.path.exists(args.output_dir): 199 | os.makedirs(args.output_dir) 200 | main(args) 201 | -------------------------------------------------------------------------------- /scripts/train_regressor.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import argparse 4 | import pickle 5 | import random 6 | 7 | import numpy as np 8 | from scipy.optimize import minimize 9 | from scipy.stats import pearsonr 10 | from sklearn.metrics import mean_squared_error 11 | import sklearn.linear_model as linear_model 12 | import sklearn.svm as svm 13 | import sklearn.ensemble as ensemble 14 | import sklearn.kernel_ridge as kernel_ridge 15 | import sklearn.isotonic as isotonic 16 | import sklearn.gaussian_process as gaussian_process 17 | from sklearn.preprocessing import PolynomialFeatures 18 | from sklearn.pipeline import Pipeline 19 | from sklearn.model_selection import GridSearchCV 20 | from sklearn.decomposition import PCA 21 | import imageio 22 | import joblib 23 | 24 | from utils import normalize_dataset 25 | 26 | 27 | def main(args): 28 | model_dir = os.path.join( 29 | args.output_dir, "models/{}".format("all/" if args.no_split else "")) 30 | if not os.path.exists(model_dir): 31 | os.makedirs(model_dir) 32 | 33 | db_dict = pickle.load(open(args.db_file, "rb")) 34 | db_dict = normalize_dataset(db_dict) 35 | db_train = db_dict["data"]["all" if args.no_split else "train"] 36 | db_test = db_dict["data"]["test"] 37 | features = np.load(open(args.features, "rb"), allow_pickle=True).item() 38 | 39 | if args.db_extra_file is not None: 40 | assert args.features_extra is not None 41 | db_extra_dict = pickle.load(open(args.db_extra_file, "rb")) 42 | db_extra_dict = normalize_dataset(db_extra_dict) 43 | db_extra_train = db_extra_dict["data"]["all" if args.no_split else "train"] 44 | 45 | # # normalize datasets 46 | # db_extra_train_keys = db_extra_train.keys() 47 | # db_train_keys = db_train.keys() 48 | # if len(db_extra_train_keys) > len(db_train_keys): 49 | # db_extra_train_keys = random.sample( 50 | # db_extra_train_keys, len(db_train_keys)) 51 | # db_extra_train = {key: db_extra_train_keys[key] 52 | # for key in db_extra_train_keys} 53 | # else: 54 | # db_train_keys = random.sample( 55 | # db_train_keys, len(db_extra_train_keys)) 56 | # db_train = {key: db_train[key] for key in db_train_keys} 57 | 58 | db_train.update(db_extra_train) 59 | features_extra = np.load( 60 | open(args.features_extra, "rb"), allow_pickle=True).item() 61 | features.update(features_extra) 62 | 63 | features_train = [features[profile_id] 64 | for profile_id in db_train.keys()] 65 | features_test = [features[profile_id] for profile_id in db_test.keys()] 66 | nan_idx_train = np.argwhere(np.isnan(features_train)) 67 | nan_idx_test = np.argwhere(np.isnan(features_test)) 68 | nan_idx_train = np.unique(nan_idx_train[:, 0]) 69 | nan_idx_test = np.unique(nan_idx_test[:, 0]) 70 | print("Found {}/{} images without faces in train split".format(len(nan_idx_train), len(db_train))) 71 | print("Found {}/{} images without faces in test split".format(len(nan_idx_test), len(db_test))) 72 | features_train = np.delete(features_train, nan_idx_train, axis=0) 73 | features_test = np.delete(features_test, nan_idx_test, axis=0) 74 | db_flat_train = np.delete(list(db_train.values()), nan_idx_train) 75 | db_flat_test = np.delete(list(db_test.values()), nan_idx_test) 76 | 77 | scores_train = [d["score"] for d in db_flat_train] 78 | scores_test = [d["score"] for d in db_flat_test] 79 | 80 | # uncomment regressors to train 81 | model_info_list = ( 82 | (linear_model.LinearRegression,), 83 | (linear_model.Lasso, dict(type="CONT", init=dict(alpha=1e-6))), 84 | (linear_model.Ridge, dict(type="CONT", init=dict(alpha=1e-6))), 85 | # (linear_model.BayesianRidge, dict(type="CONT", init=dict( 86 | # alpha_1=1e-06, alpha_2=1e-06, lambda_1=1e-06, lambda_2=1e-06))), 87 | # (linear_model.SGDRegressor,), 88 | # (linear_model.ElasticNet,), # const 89 | # (linear_model.ARDRegression,), 90 | # (linear_model.HuberRegressor,), 91 | # (linear_model.Lars,), 92 | # (linear_model.LassoLars,), 93 | # (linear_model.PassiveAggressiveRegressor,), 94 | # (linear_model.TheilSenRegressor,), 95 | # (kernel_ridge.KernelRidge, dict(type="CONT", init=dict( 96 | # alpha=1), kwargs=dict(kernel="sigmoid"))), 97 | # (svm.SVR,), 98 | # (ensemble.AdaBoostRegressor,), 99 | # (ensemble.GradientBoostingRegressor,), 100 | # (ensemble.RandomForestRegressor,), 101 | # (gaussian_process.GaussianProcessRegressor, 102 | # dict(type="CONT", init=dict(alpha=1e-10))), 103 | ) 104 | for model_info in model_info_list: 105 | model_class = model_info[0] 106 | print("-" * 50) 107 | for i in range(args.max_order): 108 | order = i + 1 109 | print("Fitting {} w/ features of order {}".format(model_class.__name__, order)) 110 | 111 | def get_model(kwargs={}): 112 | kwargs.update(meta.get("kwargs", {})) 113 | poly = PolynomialFeatures(order) 114 | model = model_class(*meta.get("args", {}), 115 | **kwargs) 116 | if args.pca: 117 | pca = PCA(n_components=args.pca) 118 | pipeline_list = [ 119 | ("poly", poly), ("pca", pca), ("fit", model)] 120 | else: 121 | pipeline_list = [("poly", poly), ("fit", model)] 122 | return Pipeline(pipeline_list) 123 | 124 | if len(model_info) == 2: 125 | meta = model_info[1] 126 | else: 127 | meta = {} 128 | if meta.get("type") is None: 129 | print("Constant params") 130 | model = get_model() 131 | model.fit(features_train, scores_train) 132 | elif meta["type"] == "GRID": 133 | print("Finding optimal params from grid") 134 | param_grid = {"fit__" + k: v for k, v in meta["grid"].items()} 135 | model = get_model() 136 | model = GridSearchCV(model, param_grid=param_grid,).fit( 137 | features_train, scores_train).best_estimator_ 138 | print(model) 139 | elif meta["type"] == "CONT": 140 | print("Optimizing continuous params") 141 | init = meta["init"] 142 | 143 | def func(x): 144 | kwargs = {k: x[i] for i, k in enumerate(init.keys())} 145 | model = get_model(kwargs) 146 | model.fit(features_train, scores_train) 147 | scores_pred = model.predict(features_train) 148 | mean_squared_error = pearsonr(scores_pred, scores_train)[0] 149 | return mean_squared_error 150 | res = minimize(func, list(init.values()), method="Nelder-Mead") 151 | print(res) 152 | res_kwargs = {k: res.x[i] for i, k in enumerate(init.keys())} 153 | model = get_model(res_kwargs) 154 | model.fit(features_train, scores_train) 155 | else: 156 | raise ValueError 157 | 158 | if not args.no_split: 159 | scores_pred = model.predict(features_test) 160 | pc = pearsonr(scores_pred, scores_test)[0] 161 | print("PC: {:.3f}".format(pc)) 162 | 163 | name = fullname(model_class) 164 | if args.pca: 165 | name += "PCA{}".format(args.pca) 166 | model_path = os.path.join( 167 | model_dir, "{}_{}.pkl".format(name, order)) 168 | print("Saving model to {}".format(model_path)) 169 | joblib.dump(model, model_path) 170 | 171 | 172 | def fullname(cls): 173 | module = cls.__module__ 174 | if module is None or module == str.__class__.__module__: 175 | return cls.__name__ # Avoid reporting __builtin__ 176 | else: 177 | return module + "." + cls.__name__ 178 | 179 | 180 | def parse_args(): 181 | parser = argparse.ArgumentParser() 182 | parser.add_argument( 183 | "--db", 184 | dest="db_file", 185 | type=str 186 | ) 187 | parser.add_argument( 188 | "--db-extra", 189 | dest="db_extra_file", 190 | type=str 191 | ) 192 | parser.add_argument( 193 | "--output-dir", 194 | dest="output_dir", 195 | default="data", 196 | type=str 197 | ) 198 | parser.add_argument( 199 | "--features", 200 | dest="features", 201 | default="data/mtcnn-facenet/features.npy", 202 | type=str 203 | ) 204 | parser.add_argument( 205 | "--features-extra", 206 | dest="features_extra", 207 | type=str 208 | ) 209 | parser.add_argument( 210 | "--no-split", 211 | dest="no_split", 212 | action="store_true", 213 | default=False, 214 | ) 215 | parser.add_argument( 216 | "--pca", 217 | dest="pca", 218 | default=0, 219 | type=int 220 | ) 221 | parser.add_argument( 222 | "--max-order", 223 | dest="max_order", 224 | default=2, 225 | type=int 226 | ) 227 | return parser.parse_args() 228 | 229 | 230 | if __name__ == "__main__": 231 | args = parse_args() 232 | if not os.path.exists(args.output_dir): 233 | os.makedirs(args.output_dir) 234 | main(args) 235 | -------------------------------------------------------------------------------- /facial_beauty_predictor/backbone/mtcnn/layer_factory.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from distutils.version import LooseVersion 3 | 4 | 5 | class LayerFactory(object): 6 | """ 7 | Allows to create stack layers for a given network. 8 | """ 9 | 10 | AVAILABLE_PADDINGS = ('SAME', 'VALID') 11 | 12 | def __init__(self, network): 13 | self.__network = network 14 | 15 | @staticmethod 16 | def __validate_padding(padding): 17 | if padding not in LayerFactory.AVAILABLE_PADDINGS: 18 | raise Exception("Padding {} not valid".format(padding)) 19 | 20 | @staticmethod 21 | def __validate_grouping(channels_input: int, channels_output: int, group: int): 22 | if channels_input % group != 0: 23 | raise Exception( 24 | "The number of channels in the input does not match the group") 25 | 26 | if channels_output % group != 0: 27 | raise Exception( 28 | "The number of channels in the output does not match the group") 29 | 30 | @staticmethod 31 | def vectorize_input(input_layer): 32 | input_shape = input_layer.get_shape() 33 | 34 | if input_shape.ndims == 4: 35 | # Spatial input, must be vectorized. 36 | dim = 1 37 | for x in input_shape[1:].as_list(): 38 | dim *= int(x) 39 | 40 | #dim = operator.mul(*(input_shape[1:].as_list())) 41 | vectorized_input = tf.reshape(input_layer, [-1, dim]) 42 | else: 43 | vectorized_input, dim = (input_layer, input_shape[-1]) 44 | 45 | return vectorized_input, dim 46 | 47 | def __make_var(self, name: str, shape: list): 48 | """ 49 | Creates a tensorflow variable with the given name and shape. 50 | :param name: name to set for the variable. 51 | :param shape: list defining the shape of the variable. 52 | :return: created TF variable. 53 | """ 54 | return tf.compat.v1.get_variable(name, shape, trainable=self.__network.is_trainable(), 55 | use_resource=False) 56 | 57 | def new_feed(self, name: str, layer_shape: tuple): 58 | """ 59 | Creates a feed layer. This is usually the first layer in the network. 60 | :param name: name of the layer 61 | :return: 62 | """ 63 | 64 | feed_data = tf.compat.v1.placeholder(tf.float32, layer_shape, 'input') 65 | self.__network.add_layer(name, layer_output=feed_data) 66 | 67 | def new_conv(self, name: str, kernel_size: tuple, channels_output: int, 68 | stride_size: tuple, padding: str = 'SAME', 69 | group: int = 1, biased: bool = True, relu: bool = True, input_layer_name: str = None): 70 | """ 71 | Creates a convolution layer for the network. 72 | :param name: name for the layer 73 | :param kernel_size: tuple containing the size of the kernel (Width, Height) 74 | :param channels_output: ¿? Perhaps number of channels in the output? it is used as the bias size. 75 | :param stride_size: tuple containing the size of the stride (Width, Height) 76 | :param padding: Type of padding. Available values are: ('SAME', 'VALID') 77 | :param group: groups for the kernel operation. More info required. 78 | :param biased: boolean flag to set if biased or not. 79 | :param relu: boolean flag to set if ReLu should be applied at the end of the layer or not. 80 | :param input_layer_name: name of the input layer for this layer. If None, it will take the last added layer of 81 | the network. 82 | """ 83 | 84 | # Verify that the padding is acceptable 85 | self.__validate_padding(padding) 86 | 87 | input_layer = self.__network.get_layer(input_layer_name) 88 | 89 | # Get the number of channels in the input 90 | channels_input = int(input_layer.get_shape()[-1]) 91 | 92 | # Verify that the grouping parameter is valid 93 | self.__validate_grouping(channels_input, channels_output, group) 94 | 95 | # Convolution for a given input and kernel 96 | def convolve(input_val, kernel): return tf.nn.conv2d(input=input_val, 97 | filters=kernel, 98 | strides=[ 99 | 1, stride_size[1], stride_size[0], 1], 100 | padding=padding) 101 | 102 | with tf.compat.v1.variable_scope(name) as scope: 103 | kernel = self.__make_var('weights', shape=[ 104 | kernel_size[1], kernel_size[0], channels_input // group, channels_output]) 105 | 106 | output = convolve(input_layer, kernel) 107 | 108 | # Add the biases, if required 109 | if biased: 110 | biases = self.__make_var('biases', [channels_output]) 111 | output = tf.nn.bias_add(output, biases) 112 | 113 | # Apply ReLU non-linearity, if required 114 | if relu: 115 | output = tf.nn.relu(output, name=scope.name) 116 | 117 | self.__network.add_layer(name, layer_output=output) 118 | 119 | def new_prelu(self, name: str, input_layer_name: str = None): 120 | """ 121 | Creates a new prelu layer with the given name and input. 122 | :param name: name for this layer. 123 | :param input_layer_name: name of the layer that serves as input for this one. 124 | """ 125 | input_layer = self.__network.get_layer(input_layer_name) 126 | 127 | with tf.compat.v1.variable_scope(name): 128 | channels_input = int(input_layer.get_shape()[-1]) 129 | alpha = self.__make_var('alpha', shape=[channels_input]) 130 | output = tf.nn.relu(input_layer) + \ 131 | tf.multiply(alpha, -tf.nn.relu(-input_layer)) 132 | 133 | self.__network.add_layer(name, layer_output=output) 134 | 135 | def new_max_pool(self, name: str, kernel_size: tuple, stride_size: tuple, padding='SAME', 136 | input_layer_name: str = None): 137 | """ 138 | Creates a new max pooling layer. 139 | :param name: name for the layer. 140 | :param kernel_size: tuple containing the size of the kernel (Width, Height) 141 | :param stride_size: tuple containing the size of the stride (Width, Height) 142 | :param padding: Type of padding. Available values are: ('SAME', 'VALID') 143 | :param input_layer_name: name of the input layer for this layer. If None, it will take the last added layer of 144 | the network. 145 | """ 146 | 147 | self.__validate_padding(padding) 148 | 149 | input_layer = self.__network.get_layer(input_layer_name) 150 | 151 | output = tf.nn.max_pool2d(input=input_layer, 152 | ksize=[1, kernel_size[1], kernel_size[0], 1], 153 | strides=[1, stride_size[1], 154 | stride_size[0], 1], 155 | padding=padding, 156 | name=name) 157 | 158 | self.__network.add_layer(name, layer_output=output) 159 | 160 | def new_fully_connected(self, name: str, output_count: int, relu=True, input_layer_name: str = None): 161 | """ 162 | Creates a new fully connected layer. 163 | 164 | :param name: name for the layer. 165 | :param output_count: number of outputs of the fully connected layer. 166 | :param relu: boolean flag to set if ReLu should be applied at the end of this layer. 167 | :param input_layer_name: name of the input layer for this layer. If None, it will take the last added layer of 168 | the network. 169 | """ 170 | 171 | with tf.compat.v1.variable_scope(name): 172 | input_layer = self.__network.get_layer(input_layer_name) 173 | vectorized_input, dimension = self.vectorize_input(input_layer) 174 | 175 | weights = self.__make_var( 176 | 'weights', shape=[dimension, output_count]) 177 | biases = self.__make_var('biases', shape=[output_count]) 178 | operation = tf.compat.v1.nn.relu_layer if relu else tf.compat.v1.nn.xw_plus_b 179 | 180 | fc = operation(vectorized_input, weights, biases, name=name) 181 | 182 | self.__network.add_layer(name, layer_output=fc) 183 | 184 | def new_softmax(self, name, axis, input_layer_name: str = None): 185 | """ 186 | Creates a new softmax layer 187 | :param name: name to set for the layer 188 | :param axis: 189 | :param input_layer_name: name of the input layer for this layer. If None, it will take the last added layer of 190 | the network. 191 | """ 192 | input_layer = self.__network.get_layer(input_layer_name) 193 | 194 | if LooseVersion(tf.__version__) < LooseVersion("1.5.0"): 195 | max_axis = tf.reduce_max( 196 | input_tensor=input_layer, axis=axis, keepdims=True) 197 | target_exp = tf.exp(input_layer - max_axis) 198 | normalize = tf.reduce_sum( 199 | input_tensor=target_exp, axis=axis, keepdims=True) 200 | else: 201 | max_axis = tf.reduce_max( 202 | input_tensor=input_layer, axis=axis, keepdims=True) 203 | target_exp = tf.exp(input_layer - max_axis) 204 | normalize = tf.reduce_sum( 205 | input_tensor=target_exp, axis=axis, keepdims=True) 206 | 207 | softmax = tf.math.divide(target_exp, normalize, name) 208 | 209 | self.__network.add_layer(name, layer_output=softmax) 210 | -------------------------------------------------------------------------------- /facial_beauty_predictor/worker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import time 4 | from queue import Queue 5 | from threading import Thread 6 | import logging 7 | import requests 8 | from io import BytesIO 9 | from concurrent.futures import ThreadPoolExecutor 10 | import time 11 | 12 | import tensorflow as tf 13 | from skimage.transform import resize 14 | import numpy as np 15 | import imageio 16 | 17 | from facial_beauty_predictor.backbone.mtcnn.mtcnn import MTCNN 18 | import facial_beauty_predictor.backbone.facenet as facenet 19 | import facial_beauty_predictor.utils.img as img_utils 20 | 21 | MARGIN = 44 22 | FACE_IMAGE_SIZE = 182 23 | IMAGE_BATCH = 1000 24 | 25 | 26 | def worker_mtcnn_facenet_async(db, facenet_model_path, skip_multiple_faces=False): 27 | # img_paths -> img_list -> croped_faces -> features -> avg_features 28 | img_paths_queue = Queue() 29 | pipeline = PipelineJoinable(img_paths_queue, 30 | image_loader_async, 31 | (mtcnn_face_detector_async, dict(kwargs=dict( 32 | skip_multiple_faces=skip_multiple_faces))), 33 | (gen_facenet_features_async, 34 | dict(args=[facenet_model_path])), 35 | calc_avg_features_async) 36 | for profile_id, profile in db.items(): 37 | img_paths_queue.put(dict(id=profile_id, paths=profile["img_paths"])) 38 | pipeline.join() 39 | features_list = pipeline.get_output() 40 | features_dict = {features["id"]: features["features"] 41 | for features in features_list} 42 | 43 | return features_dict 44 | 45 | 46 | def worker_mtcnn_facenet_async_queue(img_paths_queue, facenet_model_path, regressor_model, percentiles, skip_multiple_faces=True): 47 | # img_paths -> img_list -> croped_faces -> features -> avg_features 48 | pipeline = Pipeline(img_paths_queue, 49 | (image_loader_async, dict(kwargs=dict(local_files=False))), 50 | (mtcnn_face_detector_async, dict(kwargs=dict( 51 | skip_multiple_faces=skip_multiple_faces))), 52 | (gen_facenet_features_async, 53 | dict(args=[facenet_model_path])), 54 | calc_avg_features_async, 55 | (regress_score_async, dict(args=[regressor_model])), 56 | (calc_percentile, dict(args=[percentiles]))) 57 | return pipeline.out_queue 58 | 59 | 60 | def worker_mtcnn_async(db): 61 | # img_paths -> img_list -> features -> avg_features 62 | img_paths_queue = Queue() 63 | pipeline = PipelineJoinable(img_paths_queue, 64 | image_loader_async, 65 | mtcnn_features_async, 66 | calc_avg_features_async) 67 | for profile_id, profile in db.items(): 68 | img_paths_queue.put(dict(id=profile_id, paths=profile["img_paths"])) 69 | pipeline.join() 70 | features_list = pipeline.get_output() 71 | features_dict = {features["id"]: features["features"] 72 | for features in features_list} 73 | 74 | return features_dict 75 | 76 | 77 | def worker_mtcnn_facenet_2_async(db, facenet_model_path): 78 | # img_paths -> img_list -> croped_faces -> features -> avg_features 79 | img_paths_queue = Queue() 80 | pipeline = PipelineJoinable(img_paths_queue, 81 | image_loader_async, 82 | (mtcnn_face_detector_async, dict(kwargs=dict( 83 | skip_multiple_faces=True, store_features=True))), 84 | (gen_facenet_features_async, 85 | dict(args=[facenet_model_path])), 86 | calc_avg_features_async) 87 | for profile_id, profile in db.items(): 88 | img_paths_queue.put(dict(id=profile_id, paths=profile["img_paths"])) 89 | pipeline.join() 90 | features_list = pipeline.get_output() 91 | features_dict = {features["id"]: features["features"] 92 | for features in features_list} 93 | 94 | return features_dict 95 | 96 | 97 | class Pipeline(): 98 | def __init__(self, in_queue, *parts): 99 | self.queue_list = [in_queue] 100 | self.out_queue = in_queue 101 | for i, part in enumerate(parts): 102 | if hasattr(part, "__len__"): 103 | func = part[0] 104 | args = part[1].get("args", []) 105 | kwargs = part[1].get("kwargs", {}) 106 | else: 107 | func = part 108 | args = [] 109 | kwargs = {} 110 | in_queue = self.out_queue 111 | self.out_queue = Queue() 112 | self.queue_list.append(self.out_queue) 113 | thread = Thread( 114 | target=func, args=(in_queue, self.out_queue, *args), 115 | kwargs=kwargs, name="Pipeline{}Thread".format(i), daemon=True) 116 | thread.start() 117 | 118 | 119 | class PipelineJoinable(Pipeline): 120 | def __init__(self, in_queue, *parts): 121 | super.__init__(in_queue, *parts) 122 | self.out_list = [] 123 | thread = Thread( 124 | target=collect_async, args=(self.out_queue, self.out_list), name="PipelineCollectThread", daemon=True) 125 | thread.start() 126 | 127 | def join(self): 128 | for queue in self.queue_list: 129 | queue.join() 130 | 131 | def get_output(self): 132 | return self.out_list 133 | 134 | 135 | def image_loader_async(img_path_queue, img_list_queue, local_files=True): 136 | while True: 137 | img_path_dict = img_path_queue.get() 138 | 139 | def load_images(img_paths, profile_id): 140 | start = time.time() 141 | img_list = [] 142 | 143 | def load_image(img_path): 144 | if local_files: 145 | if os.path.exists(img_path): 146 | img = imageio.imread(img_path) 147 | img_list.append(img) 148 | else: 149 | logging.warn("{} not found".format(img_path)) 150 | else: 151 | try: 152 | response = requests.get(img_path, timeout=5) 153 | img = imageio.imread(BytesIO(response.content)) 154 | img_list.append(img) 155 | except Exception: 156 | logging.exception( 157 | "Could not download {}".format(img_path)) 158 | 159 | with ThreadPoolExecutor(thread_name_prefix="LoadImagesWorker", max_workers=max(len(img_paths), os.cpu_count() + 4)) as executor: 160 | executor.map(load_image, img_paths) 161 | 162 | img_list_queue.put(dict(id=profile_id, img_list=img_list)) 163 | img_path_queue.task_done() 164 | logging.debug("Loading {} profile images took {:.3f}s".format( 165 | len(img_list), 166 | time.time() - start)) 167 | thread = Thread(target=load_images, name="LoadImagesThread", args=( 168 | img_path_dict["paths"], img_path_dict["id"])) 169 | thread.start() 170 | 171 | 172 | def mtcnn_face_detector_async(img_list_queue, cropped_faces_queue, 173 | skip_multiple_faces=False, store_features=False): 174 | # based on https://github.com/cjekel/tindetheus/blob/master/tindetheus/facenet_clone/align/align_dataset_mtcnn.py and https://github.com/davidsandberg/facenet/blob/master/src/align/align_dataset_mtcnn.py 175 | mtcnn = MTCNN() 176 | logging.debug("MTCNN initialization done") 177 | 178 | while True: 179 | img_list_dict = img_list_queue.get() 180 | start = time.time() 181 | img_list = img_list_dict["img_list"] 182 | profile_id = img_list_dict["id"] 183 | 184 | faces = [] 185 | features = [] if store_features else None 186 | 187 | for i, img in enumerate(img_list): 188 | img = img[:, :, 0:3] 189 | bounding_boxes, _, fc1 = mtcnn.detect_faces_raw(img) 190 | n_faces = bounding_boxes.shape[0] 191 | if n_faces > 0: 192 | det = bounding_boxes[:, 0:4] 193 | img_size = np.asarray(img.shape)[0:2] 194 | if n_faces > 1: 195 | if skip_multiple_faces: 196 | continue 197 | bounding_box_size = ( 198 | det[:, 2] - det[:, 0]) * (det[:, 3] - det[:, 1]) 199 | img_center = img_size / 2 200 | offsets = np.vstack( 201 | [(det[:, 0] + det[:, 2]) / 2 - img_center[1], (det[:, 1] + det[:, 3]) / 2 - img_center[0]]) 202 | offset_dist_squared = np.sum( 203 | np.power(offsets, 2.0), 0) 204 | # some extra weight on the centering 205 | index = np.argmax( 206 | bounding_box_size - offset_dist_squared * 2.0) 207 | det = det[index, :] 208 | det = np.squeeze(det) 209 | bb = np.zeros(4, dtype=np.int32) 210 | bb[0] = np.maximum(det[0] - MARGIN / 2, 0) 211 | bb[1] = np.maximum(det[1] - MARGIN / 2, 0) 212 | bb[2] = np.minimum(det[2] + MARGIN / 2, img_size[1]) 213 | bb[3] = np.minimum(det[3] + MARGIN / 2, img_size[0]) 214 | cropped = img[bb[1]:bb[3], bb[0]:bb[2], :] 215 | scaled = resize(cropped, (FACE_IMAGE_SIZE, FACE_IMAGE_SIZE), 216 | mode='constant') 217 | faces.append(scaled) 218 | else: 219 | logging.debug( 220 | "No faces found in image nr {} of {}".format(i + 1, profile_id)) 221 | if store_features: 222 | features.append(fc1) 223 | 224 | assert len(img_list) >= len(faces) 225 | 226 | cropped_faces_queue.put( 227 | dict(id=profile_id, faces=faces, features=features)) 228 | img_list_queue.task_done() 229 | logging.debug("Detecting {} faces in {} images took {:.3f}s".format( 230 | len(faces), len(img_list), time.time() - start)) 231 | 232 | 233 | def mtcnn_features_async(img_list_queue, features_queue): 234 | mtcnn = MTCNN() 235 | logging.debug("MTCNN initialization done") 236 | 237 | while True: 238 | img_list_dict = img_list_queue.get() 239 | img_list = img_list_dict["img_list"] 240 | profile_id = img_list_dict["id"] 241 | 242 | features = [] 243 | 244 | for img in img_list: 245 | img = img[:, :, 0:3] 246 | bounding_boxes, _, fc1 = mtcnn.detect_faces_raw(img) 247 | n_faces = bounding_boxes.shape[0] 248 | if n_faces == 1: 249 | features.append(fc1) 250 | 251 | assert len(img_list) >= len(features) 252 | 253 | features_queue.put(dict(id=profile_id, features=features)) 254 | img_list_queue.task_done() 255 | 256 | 257 | def gen_facenet_features_async(cropped_faces_queue, features_queue, 258 | model_path, image_size=160): 259 | # based on https://github.com/cjekel/tindetheus/blob/master/tindetheus/export_features.py and https://github.com/davidsandberg/facenet/blob/master/src/compare.py 260 | with tf.Graph().as_default(): 261 | with tf.compat.v1.Session() as sess: 262 | # Load the model 263 | facenet.load_model(model_path) 264 | 265 | logging.debug("FaceNet initialization done") 266 | 267 | # Get input and output tensors 268 | images_placeholder = tf.compat.v1.get_default_graph().get_tensor_by_name("input:0") 269 | embeddings = tf.compat.v1.get_default_graph().get_tensor_by_name("embeddings:0") 270 | phase_train_placeholder = tf.compat.v1.get_default_graph( 271 | ).get_tensor_by_name("phase_train:0") 272 | 273 | while True: 274 | cropped_face_dict = cropped_faces_queue.get() 275 | start = time.time() 276 | img_list = cropped_face_dict["faces"] 277 | mtcnn_features = cropped_face_dict.get("features") 278 | profile_id = cropped_face_dict["id"] 279 | 280 | # Run forward pass to calculate embeddings 281 | n_images = len(img_list) 282 | 283 | # print('Number of images: ', n_images) 284 | batch_size = IMAGE_BATCH 285 | if n_images % batch_size == 0: 286 | n_batches = n_images // batch_size 287 | else: 288 | n_batches = (n_images // batch_size) + 1 289 | # print('Number of batches: ', n_batches) 290 | embedding_size = embeddings.get_shape()[1] 291 | emb_array = np.zeros((n_images, embedding_size)) 292 | # start_time = time.time() 293 | 294 | for i in range(n_batches): 295 | if i == n_batches - 1: 296 | n = n_images 297 | else: 298 | n = i * batch_size + batch_size 299 | # Get images for the batch 300 | imgs_batch = img_list[i * batch_size:n] 301 | n_samples = len(imgs_batch) 302 | images = np.zeros((n_samples, image_size, image_size, 3)) 303 | for j in range(n_samples): 304 | img = imgs_batch[j] 305 | if img.ndim == 2: 306 | img = img_utils.to_rgb(img) 307 | img = img_utils.prewhiten(img) 308 | img = img_utils.crop(img, False, image_size) 309 | img = img_utils.flip(img, False) 310 | images[j, :, :, :] = img 311 | feed_dict = {images_placeholder: images, 312 | phase_train_placeholder: False} 313 | # Use the facenet model to calculate embeddings 314 | embed = sess.run(embeddings, feed_dict=feed_dict) 315 | emb_array[i * batch_size:n, :] = embed 316 | # print('Completed batch', i+1, 'of', n_batches) 317 | 318 | if mtcnn_features is not None: 319 | if n_images: 320 | features = np.hstack((mtcnn_features, emb_array)) 321 | else: 322 | features = np.empty((0, emb_array.shape[1] + 256)) 323 | else: 324 | features = emb_array 325 | 326 | # run_time = time.time() - start_time 327 | # print('Run time: ', run_time) 328 | 329 | features_queue.put(dict(id=profile_id, features=features)) 330 | cropped_faces_queue.task_done() 331 | logging.debug("Feature vector generation for {} faces took {:.3f}s".format( 332 | n_images, time.time() - start)) 333 | 334 | 335 | def calc_avg_features_async(features_queue, avg_features_queue): 336 | # a function to create a vector of n average features for each 337 | # tinder profile 338 | 339 | while True: 340 | features_dict = features_queue.get() 341 | features = features_dict["features"] # (128, n_emb) for FaceNet 342 | profile_id = features_dict["id"] 343 | 344 | if features.shape[0]: 345 | avg_features = np.mean(features, axis=0) 346 | else: 347 | avg_features = features 348 | 349 | avg_features_queue.put( 350 | dict(id=profile_id, features=avg_features)) 351 | features_queue.task_done() 352 | 353 | 354 | def regress_score_async(features_queue, score_queue, model): 355 | while True: 356 | features_dict = features_queue.get() 357 | start = time.time() 358 | features = features_dict["features"] # (1, 128) for FaceNet 359 | profile_id = features_dict["id"] 360 | 361 | if features.shape[0]: 362 | try: 363 | features = features.reshape(1, -1) 364 | score_pred = model.predict(features) 365 | score_pred = np.clip(score_pred, 0, 1) 366 | except Exception: 367 | logging.exception("Failed to predict profile score") 368 | score_pred = None 369 | else: 370 | score_pred = 0 371 | 372 | score_queue.put( 373 | dict(id=profile_id, score=score_pred)) 374 | features_queue.task_done() 375 | logging.debug("Regression took {:.3f}s".format(time.time() - start)) 376 | 377 | 378 | def calc_percentile(score_queue, percentile_queue, percentiles): 379 | while True: 380 | score_dict = score_queue.get() 381 | profile_id = score_dict["id"] 382 | score = score_dict["score"] 383 | 384 | if score is None: 385 | percentile = None 386 | else: 387 | percentile = 1 388 | for i, percentile_score in enumerate(percentiles): 389 | if score > percentile_score: 390 | percentile = 1 - i * 0.05 391 | else: 392 | break 393 | percentile_queue.put( 394 | dict(id=profile_id, percentile=percentile)) 395 | score_queue.task_done() 396 | 397 | 398 | def collect_async(in_queue, out_list): 399 | while True: 400 | out_list.append(in_queue.get()) 401 | in_queue.task_done() 402 | in_queue.task_done() 403 | -------------------------------------------------------------------------------- /facial_beauty_predictor/backbone/mtcnn/mtcnn.py: -------------------------------------------------------------------------------- 1 | # IMPORTANT: 2 | # 3 | # This code is derivated from the MTCNN implementation of David Sandberg for Facenet 4 | # (https://github.com/davidsandberg/facenet/) 5 | # It has been rebuilt from scratch, taking the David Sandberg's implementation as a reference. 6 | # The code improves the readibility, fixes several mistakes in the definition of the network (layer names) 7 | # and provides the keypoints of faces as outputs along with the bounding boxes. 8 | # 9 | 10 | import cv2 11 | import numpy as np 12 | import pkg_resources 13 | import tensorflow as tf 14 | from facial_beauty_predictor.backbone.mtcnn.layer_factory import LayerFactory 15 | from facial_beauty_predictor.backbone.mtcnn.network import Network 16 | from facial_beauty_predictor.backbone.mtcnn.exceptions import InvalidImage 17 | 18 | 19 | class PNet(Network): 20 | """ 21 | Network to propose areas with faces. 22 | """ 23 | 24 | def _config(self): 25 | layer_factory = LayerFactory(self) 26 | 27 | layer_factory.new_feed(name='data', layer_shape=(None, None, None, 3)) 28 | layer_factory.new_conv(name='conv1', kernel_size=(3, 3), channels_output=10, stride_size=(1, 1), 29 | padding='VALID', relu=False) 30 | layer_factory.new_prelu(name='prelu1') 31 | layer_factory.new_max_pool( 32 | name='pool1', kernel_size=(2, 2), stride_size=(2, 2)) 33 | layer_factory.new_conv(name='conv2', kernel_size=(3, 3), channels_output=16, stride_size=(1, 1), 34 | padding='VALID', relu=False) 35 | layer_factory.new_prelu(name='prelu2') 36 | layer_factory.new_conv(name='conv3', kernel_size=(3, 3), channels_output=32, stride_size=(1, 1), 37 | padding='VALID', relu=False) 38 | layer_factory.new_prelu(name='prelu3') 39 | layer_factory.new_conv(name='conv4-1', kernel_size=(1, 1), 40 | channels_output=2, stride_size=(1, 1), relu=False) 41 | layer_factory.new_softmax(name='prob1', axis=3) 42 | 43 | layer_factory.new_conv(name='conv4-2', kernel_size=(1, 1), channels_output=4, stride_size=(1, 1), 44 | input_layer_name='prelu3', relu=False) 45 | 46 | def _feed(self, image): 47 | return self._session.run(['pnet/conv4-2/BiasAdd:0', 'pnet/prob1:0'], feed_dict={'pnet/input:0': image}) 48 | 49 | 50 | class RNet(Network): 51 | """ 52 | Network to refine the areas proposed by PNet 53 | """ 54 | 55 | def _config(self): 56 | 57 | layer_factory = LayerFactory(self) 58 | 59 | layer_factory.new_feed(name='data', layer_shape=(None, 24, 24, 3)) 60 | layer_factory.new_conv(name='conv1', kernel_size=(3, 3), channels_output=28, stride_size=(1, 1), 61 | padding='VALID', relu=False) 62 | layer_factory.new_prelu(name='prelu1') 63 | layer_factory.new_max_pool( 64 | name='pool1', kernel_size=(3, 3), stride_size=(2, 2)) 65 | layer_factory.new_conv(name='conv2', kernel_size=(3, 3), channels_output=48, stride_size=(1, 1), 66 | padding='VALID', relu=False) 67 | layer_factory.new_prelu(name='prelu2') 68 | layer_factory.new_max_pool(name='pool2', kernel_size=( 69 | 3, 3), stride_size=(2, 2), padding='VALID') 70 | layer_factory.new_conv(name='conv3', kernel_size=(2, 2), channels_output=64, stride_size=(1, 1), 71 | padding='VALID', relu=False) 72 | layer_factory.new_prelu(name='prelu3') 73 | layer_factory.new_fully_connected( 74 | name='fc1', output_count=128, relu=False) # shouldn't the name be "fc1"? 75 | layer_factory.new_prelu(name='prelu4') 76 | # shouldn't the name be "fc2-1"? 77 | layer_factory.new_fully_connected( 78 | name='fc2-1', output_count=2, relu=False) 79 | layer_factory.new_softmax(name='prob1', axis=1) 80 | 81 | layer_factory.new_fully_connected( 82 | name='fc2-2', output_count=4, relu=False, input_layer_name='prelu4') 83 | 84 | def _feed(self, image): 85 | return self._session.run(['rnet/fc2-2/fc2-2:0', 'rnet/prob1:0'], feed_dict={'rnet/input:0': image}) 86 | 87 | 88 | class ONet(Network): 89 | """ 90 | Network to retrieve the keypoints 91 | """ 92 | 93 | def _config(self): 94 | layer_factory = LayerFactory(self) 95 | 96 | layer_factory.new_feed(name='data', layer_shape=(None, 48, 48, 3)) 97 | layer_factory.new_conv(name='conv1', kernel_size=(3, 3), channels_output=32, stride_size=(1, 1), 98 | padding='VALID', relu=False) 99 | layer_factory.new_prelu(name='prelu1') 100 | layer_factory.new_max_pool( 101 | name='pool1', kernel_size=(3, 3), stride_size=(2, 2)) 102 | layer_factory.new_conv(name='conv2', kernel_size=(3, 3), channels_output=64, stride_size=(1, 1), 103 | padding='VALID', relu=False) 104 | layer_factory.new_prelu(name='prelu2') 105 | layer_factory.new_max_pool(name='pool2', kernel_size=( 106 | 3, 3), stride_size=(2, 2), padding='VALID') 107 | layer_factory.new_conv(name='conv3', kernel_size=(3, 3), channels_output=64, stride_size=(1, 1), 108 | padding='VALID', relu=False) 109 | layer_factory.new_prelu(name='prelu3') 110 | layer_factory.new_max_pool( 111 | name='pool3', kernel_size=(2, 2), stride_size=(2, 2)) 112 | layer_factory.new_conv(name='conv4', kernel_size=(2, 2), channels_output=128, stride_size=(1, 1), 113 | padding='VALID', relu=False) 114 | layer_factory.new_prelu(name='prelu4') 115 | layer_factory.new_fully_connected( 116 | name='fc1', output_count=256, relu=False) 117 | layer_factory.new_prelu(name='prelu5') 118 | layer_factory.new_fully_connected( 119 | name='fc2-1', output_count=2, relu=False) 120 | layer_factory.new_softmax(name='prob1', axis=1) 121 | 122 | layer_factory.new_fully_connected( 123 | name='fc2-2', output_count=4, relu=False, input_layer_name='prelu5') 124 | 125 | layer_factory.new_fully_connected( 126 | name='fc2-3', output_count=10, relu=False, input_layer_name='prelu5') 127 | 128 | def _feed(self, image): 129 | return self._session.run(['onet/fc2-2/fc2-2:0', 'onet/fc2-3/fc2-3:0', 'onet/prob1:0', 'onet/fc1/fc1:0'], 130 | feed_dict={'onet/input:0': image}) 131 | 132 | 133 | class StageStatus(object): 134 | """ 135 | Keeps status between MTCNN stages 136 | """ 137 | 138 | def __init__(self, pad_result: tuple = None, width=0, height=0): 139 | self.width = width 140 | self.height = height 141 | self.dy = self.edy = self.dx = self.edx = self.y = self.ey = self.x = self.ex = self.tmpw = self.tmph = [] 142 | 143 | if pad_result is not None: 144 | self.update(pad_result) 145 | 146 | def update(self, pad_result: tuple): 147 | s = self 148 | s.dy, s.edy, s.dx, s.edx, s.y, s.ey, s.x, s.ex, s.tmpw, s.tmph = pad_result 149 | 150 | 151 | class MTCNN(object): 152 | """ 153 | Allows to perform MTCNN Detection -> 154 | a) Detection of faces (with the confidence probability) 155 | b) Detection of keypoints (left eye, right eye, nose, mouth_left, mouth_right) 156 | """ 157 | 158 | def __init__(self, weights_file: str = None, min_face_size: int = 20, steps_threshold: list = None, 159 | scale_factor: float = 0.709): 160 | """ 161 | Initializes the MTCNN. 162 | :param weights_file: file uri with the weights of the P, R and O networks from MTCNN. By default it will load 163 | the ones bundled with the package. 164 | :param min_face_size: minimum size of the face to detect 165 | :param steps_threshold: step's thresholds values 166 | :param scale_factor: scale factor 167 | """ 168 | if steps_threshold is None: 169 | steps_threshold = [0.6, 0.7, 0.7] 170 | 171 | if weights_file is None: 172 | assert pkg_resources.resource_exists( 173 | __name__, "data/mtcnn_weights.npy") 174 | weights_file = pkg_resources.resource_stream( 175 | __name__, 'data/mtcnn_weights.npy') 176 | 177 | self.__min_face_size = min_face_size 178 | self.__steps_threshold = steps_threshold 179 | self.__scale_factor = scale_factor 180 | 181 | config = tf.compat.v1.ConfigProto(log_device_placement=False) 182 | config.gpu_options.allow_growth = True 183 | 184 | self.__graph = tf.Graph() 185 | 186 | with self.__graph.as_default(): 187 | self.__session = tf.compat.v1.Session( 188 | config=config, graph=self.__graph) 189 | 190 | weights = np.load(weights_file, allow_pickle=True).item() 191 | self.__pnet = PNet(self.__session, False) 192 | self.__pnet.set_weights(weights['PNet']) 193 | 194 | self.__rnet = RNet(self.__session, False) 195 | self.__rnet.set_weights(weights['RNet']) 196 | 197 | self.__onet = ONet(self.__session, False) 198 | self.__onet.set_weights(weights['ONet']) 199 | 200 | weights_file.close() 201 | 202 | @property 203 | def min_face_size(self): 204 | return self.__min_face_size 205 | 206 | @min_face_size.setter 207 | def min_face_size(self, mfc=20): 208 | try: 209 | self.__min_face_size = int(mfc) 210 | except ValueError: 211 | self.__min_face_size = 20 212 | 213 | def __compute_scale_pyramid(self, m, min_layer): 214 | scales = [] 215 | factor_count = 0 216 | 217 | while min_layer >= 12: 218 | scales += [m * np.power(self.__scale_factor, factor_count)] 219 | min_layer = min_layer * self.__scale_factor 220 | factor_count += 1 221 | 222 | return scales 223 | 224 | @staticmethod 225 | def __scale_image(image, scale: float): 226 | """ 227 | Scales the image to a given scale. 228 | :param image: 229 | :param scale: 230 | :return: 231 | """ 232 | height, width, _ = image.shape 233 | 234 | width_scaled = int(np.ceil(width * scale)) 235 | height_scaled = int(np.ceil(height * scale)) 236 | 237 | im_data = cv2.resize( 238 | image, (width_scaled, height_scaled), interpolation=cv2.INTER_AREA) 239 | 240 | # Normalize the image's pixels 241 | im_data_normalized = (im_data - 127.5) * 0.0078125 242 | 243 | return im_data_normalized 244 | 245 | @staticmethod 246 | def __generate_bounding_box(imap, reg, scale, t): 247 | 248 | # use heatmap to generate bounding boxes 249 | stride = 2 250 | cellsize = 12 251 | 252 | imap = np.transpose(imap) 253 | dx1 = np.transpose(reg[:, :, 0]) 254 | dy1 = np.transpose(reg[:, :, 1]) 255 | dx2 = np.transpose(reg[:, :, 2]) 256 | dy2 = np.transpose(reg[:, :, 3]) 257 | 258 | y, x = np.where(imap >= t) 259 | 260 | if y.shape[0] == 1: 261 | dx1 = np.flipud(dx1) 262 | dy1 = np.flipud(dy1) 263 | dx2 = np.flipud(dx2) 264 | dy2 = np.flipud(dy2) 265 | 266 | score = imap[(y, x)] 267 | reg = np.transpose( 268 | np.vstack([dx1[(y, x)], dy1[(y, x)], dx2[(y, x)], dy2[(y, x)]])) 269 | 270 | if reg.size == 0: 271 | reg = np.empty(shape=(0, 3)) 272 | 273 | bb = np.transpose(np.vstack([y, x])) 274 | 275 | q1 = np.fix((stride * bb + 1)/scale) 276 | q2 = np.fix((stride * bb + cellsize)/scale) 277 | boundingbox = np.hstack([q1, q2, np.expand_dims(score, 1), reg]) 278 | 279 | return boundingbox, reg 280 | 281 | @staticmethod 282 | def __nms(boxes, threshold, method): 283 | """ 284 | Non Maximum Suppression. 285 | 286 | :param boxes: np array with bounding boxes. 287 | :param threshold: 288 | :param method: NMS method to apply. Available values ('Min', 'Union') 289 | :return: 290 | """ 291 | if boxes.size == 0: 292 | return np.empty((0, 3)) 293 | 294 | x1 = boxes[:, 0] 295 | y1 = boxes[:, 1] 296 | x2 = boxes[:, 2] 297 | y2 = boxes[:, 3] 298 | s = boxes[:, 4] 299 | 300 | area = (x2 - x1 + 1) * (y2 - y1 + 1) 301 | sorted_s = np.argsort(s) 302 | 303 | pick = np.zeros_like(s, dtype=np.int16) 304 | counter = 0 305 | while sorted_s.size > 0: 306 | i = sorted_s[-1] 307 | pick[counter] = i 308 | counter += 1 309 | idx = sorted_s[0:-1] 310 | 311 | xx1 = np.maximum(x1[i], x1[idx]) 312 | yy1 = np.maximum(y1[i], y1[idx]) 313 | xx2 = np.minimum(x2[i], x2[idx]) 314 | yy2 = np.minimum(y2[i], y2[idx]) 315 | 316 | w = np.maximum(0.0, xx2 - xx1 + 1) 317 | h = np.maximum(0.0, yy2 - yy1 + 1) 318 | 319 | inter = w * h 320 | 321 | if method is 'Min': 322 | o = inter / np.minimum(area[i], area[idx]) 323 | else: 324 | o = inter / (area[i] + area[idx] - inter) 325 | 326 | sorted_s = sorted_s[np.where(o <= threshold)] 327 | 328 | pick = pick[0:counter] 329 | 330 | return pick 331 | 332 | @staticmethod 333 | def __pad(total_boxes, w, h): 334 | # compute the padding coordinates (pad the bounding boxes to square) 335 | tmpw = (total_boxes[:, 2] - total_boxes[:, 0] + 1).astype(np.int32) 336 | tmph = (total_boxes[:, 3] - total_boxes[:, 1] + 1).astype(np.int32) 337 | numbox = total_boxes.shape[0] 338 | 339 | dx = np.ones(numbox, dtype=np.int32) 340 | dy = np.ones(numbox, dtype=np.int32) 341 | edx = tmpw.copy().astype(np.int32) 342 | edy = tmph.copy().astype(np.int32) 343 | 344 | x = total_boxes[:, 0].copy().astype(np.int32) 345 | y = total_boxes[:, 1].copy().astype(np.int32) 346 | ex = total_boxes[:, 2].copy().astype(np.int32) 347 | ey = total_boxes[:, 3].copy().astype(np.int32) 348 | 349 | tmp = np.where(ex > w) 350 | edx.flat[tmp] = np.expand_dims(-ex[tmp] + w + tmpw[tmp], 1) 351 | ex[tmp] = w 352 | 353 | tmp = np.where(ey > h) 354 | edy.flat[tmp] = np.expand_dims(-ey[tmp] + h + tmph[tmp], 1) 355 | ey[tmp] = h 356 | 357 | tmp = np.where(x < 1) 358 | dx.flat[tmp] = np.expand_dims(2 - x[tmp], 1) 359 | x[tmp] = 1 360 | 361 | tmp = np.where(y < 1) 362 | dy.flat[tmp] = np.expand_dims(2 - y[tmp], 1) 363 | y[tmp] = 1 364 | 365 | return dy, edy, dx, edx, y, ey, x, ex, tmpw, tmph 366 | 367 | @staticmethod 368 | def __rerec(bbox): 369 | # convert bbox to square 370 | h = bbox[:, 3] - bbox[:, 1] 371 | w = bbox[:, 2] - bbox[:, 0] 372 | l = np.maximum(w, h) 373 | bbox[:, 0] = bbox[:, 0] + w * 0.5 - l * 0.5 374 | bbox[:, 1] = bbox[:, 1] + h * 0.5 - l * 0.5 375 | bbox[:, 2:4] = bbox[:, 0:2] + np.transpose(np.tile(l, (2, 1))) 376 | return bbox 377 | 378 | @staticmethod 379 | def __bbreg(boundingbox, reg): 380 | # calibrate bounding boxes 381 | if reg.shape[1] == 1: 382 | reg = np.reshape(reg, (reg.shape[2], reg.shape[3])) 383 | 384 | w = boundingbox[:, 2] - boundingbox[:, 0] + 1 385 | h = boundingbox[:, 3] - boundingbox[:, 1] + 1 386 | b1 = boundingbox[:, 0] + reg[:, 0] * w 387 | b2 = boundingbox[:, 1] + reg[:, 1] * h 388 | b3 = boundingbox[:, 2] + reg[:, 2] * w 389 | b4 = boundingbox[:, 3] + reg[:, 3] * h 390 | boundingbox[:, 0:4] = np.transpose(np.vstack([b1, b2, b3, b4])) 391 | return boundingbox 392 | 393 | def detect_faces(self, img) -> list: 394 | """ 395 | Detects bounding boxes from the specified image. 396 | :param img: image to process 397 | :return: list containing all the bounding boxes detected with their keypoints. 398 | """ 399 | 400 | total_boxes, points, _ = self.detect_faces_raw(img) 401 | 402 | bounding_boxes = [] 403 | 404 | for bounding_box, keypoints in zip(total_boxes, points.T): 405 | 406 | bounding_boxes.append({ 407 | 'box': [int(bounding_box[0]), int(bounding_box[1]), 408 | int(bounding_box[2] - bounding_box[0]), int(bounding_box[3] - bounding_box[1])], 409 | 'confidence': bounding_box[-1], 410 | 'keypoints': { 411 | 'left_eye': (int(keypoints[0]), int(keypoints[5])), 412 | 'right_eye': (int(keypoints[1]), int(keypoints[6])), 413 | 'nose': (int(keypoints[2]), int(keypoints[7])), 414 | 'mouth_left': (int(keypoints[3]), int(keypoints[8])), 415 | 'mouth_right': (int(keypoints[4]), int(keypoints[9])), 416 | } 417 | } 418 | ) 419 | 420 | return bounding_boxes 421 | 422 | def detect_faces_raw(self, img): 423 | if img is None or not hasattr(img, "shape"): 424 | raise InvalidImage("Image not valid.") 425 | 426 | height, width, _ = img.shape 427 | stage_status = StageStatus(width=width, height=height) 428 | 429 | m = 12 / self.__min_face_size 430 | min_layer = np.amin([height, width]) * m 431 | 432 | scales = self.__compute_scale_pyramid(m, min_layer) 433 | 434 | stages = [self.__stage1, self.__stage2, self.__stage3] 435 | result = [scales, stage_status] 436 | 437 | # We pipe here each of the stages 438 | for stage in stages: 439 | result = stage(img, result[0], result[1]) 440 | 441 | return result 442 | 443 | def __stage1(self, image, scales: list, stage_status: StageStatus): 444 | """ 445 | First stage of the MTCNN. 446 | :param image: 447 | :param scales: 448 | :param stage_status: 449 | :return: 450 | """ 451 | total_boxes = np.empty((0, 9)) 452 | status = stage_status 453 | 454 | for scale in scales: 455 | scaled_image = self.__scale_image(image, scale) 456 | 457 | img_x = np.expand_dims(scaled_image, 0) 458 | img_y = np.transpose(img_x, (0, 2, 1, 3)) 459 | 460 | out = self.__pnet.feed(img_y) 461 | 462 | out0 = np.transpose(out[0], (0, 2, 1, 3)) 463 | out1 = np.transpose(out[1], (0, 2, 1, 3)) 464 | 465 | boxes, _ = self.__generate_bounding_box(out1[0, :, :, 1].copy(), 466 | out0[0, :, :, :].copy(), scale, self.__steps_threshold[0]) 467 | 468 | # inter-scale nms 469 | pick = self.__nms(boxes.copy(), 0.5, 'Union') 470 | if boxes.size > 0 and pick.size > 0: 471 | boxes = boxes[pick, :] 472 | total_boxes = np.append(total_boxes, boxes, axis=0) 473 | 474 | numboxes = total_boxes.shape[0] 475 | 476 | if numboxes > 0: 477 | pick = self.__nms(total_boxes.copy(), 0.7, 'Union') 478 | total_boxes = total_boxes[pick, :] 479 | 480 | regw = total_boxes[:, 2] - total_boxes[:, 0] 481 | regh = total_boxes[:, 3] - total_boxes[:, 1] 482 | 483 | qq1 = total_boxes[:, 0] + total_boxes[:, 5] * regw 484 | qq2 = total_boxes[:, 1] + total_boxes[:, 6] * regh 485 | qq3 = total_boxes[:, 2] + total_boxes[:, 7] * regw 486 | qq4 = total_boxes[:, 3] + total_boxes[:, 8] * regh 487 | 488 | total_boxes = np.transpose( 489 | np.vstack([qq1, qq2, qq3, qq4, total_boxes[:, 4]])) 490 | total_boxes = self.__rerec(total_boxes.copy()) 491 | 492 | total_boxes[:, 0:4] = np.fix(total_boxes[:, 0:4]).astype(np.int32) 493 | status = StageStatus(self.__pad(total_boxes.copy(), stage_status.width, stage_status.height), 494 | width=stage_status.width, height=stage_status.height) 495 | 496 | return total_boxes, status 497 | 498 | def __stage2(self, img, total_boxes, stage_status: StageStatus): 499 | """ 500 | Second stage of the MTCNN. 501 | :param img: 502 | :param total_boxes: 503 | :param stage_status: 504 | :return: 505 | """ 506 | 507 | num_boxes = total_boxes.shape[0] 508 | if num_boxes == 0: 509 | return total_boxes, stage_status 510 | 511 | # second stage 512 | tempimg = np.zeros(shape=(24, 24, 3, num_boxes)) 513 | 514 | for k in range(0, num_boxes): 515 | tmp = np.zeros( 516 | (int(stage_status.tmph[k]), int(stage_status.tmpw[k]), 3)) 517 | 518 | tmp[stage_status.dy[k] - 1:stage_status.edy[k], stage_status.dx[k] - 1:stage_status.edx[k], :] = \ 519 | img[stage_status.y[k] - 1:stage_status.ey[k], 520 | stage_status.x[k] - 1:stage_status.ex[k], :] 521 | 522 | if tmp.shape[0] > 0 and tmp.shape[1] > 0 or tmp.shape[0] == 0 and tmp.shape[1] == 0: 523 | tempimg[:, :, :, k] = cv2.resize( 524 | tmp, (24, 24), interpolation=cv2.INTER_AREA) 525 | 526 | else: 527 | return np.empty(shape=(0,)), stage_status 528 | 529 | tempimg = (tempimg - 127.5) * 0.0078125 530 | tempimg1 = np.transpose(tempimg, (3, 1, 0, 2)) 531 | 532 | out = self.__rnet.feed(tempimg1) 533 | 534 | out0 = np.transpose(out[0]) 535 | out1 = np.transpose(out[1]) 536 | 537 | score = out1[1, :] 538 | 539 | ipass = np.where(score > self.__steps_threshold[1]) 540 | 541 | total_boxes = np.hstack( 542 | [total_boxes[ipass[0], 0:4].copy(), np.expand_dims(score[ipass].copy(), 1)]) 543 | 544 | mv = out0[:, ipass[0]] 545 | 546 | if total_boxes.shape[0] > 0: 547 | pick = self.__nms(total_boxes, 0.7, 'Union') 548 | total_boxes = total_boxes[pick, :] 549 | total_boxes = self.__bbreg( 550 | total_boxes.copy(), np.transpose(mv[:, pick])) 551 | total_boxes = self.__rerec(total_boxes.copy()) 552 | 553 | return total_boxes, stage_status 554 | 555 | def __stage3(self, img, total_boxes, stage_status: StageStatus): 556 | """ 557 | Third stage of the MTCNN. 558 | 559 | :param img: 560 | :param total_boxes: 561 | :param stage_status: 562 | :return: 563 | """ 564 | num_boxes = total_boxes.shape[0] 565 | if num_boxes == 0: 566 | return total_boxes, np.empty(shape=(0,)), np.empty(shape=(0,)) 567 | 568 | total_boxes = np.fix(total_boxes).astype(np.int32) 569 | 570 | status = StageStatus(self.__pad(total_boxes.copy(), stage_status.width, stage_status.height), 571 | width=stage_status.width, height=stage_status.height) 572 | 573 | tempimg = np.zeros((48, 48, 3, num_boxes)) 574 | 575 | for k in range(0, num_boxes): 576 | 577 | tmp = np.zeros((int(status.tmph[k]), int(status.tmpw[k]), 3)) 578 | 579 | tmp[status.dy[k] - 1:status.edy[k], status.dx[k] - 1:status.edx[k], :] = \ 580 | img[status.y[k] - 1:status.ey[k], 581 | status.x[k] - 1:status.ex[k], :] 582 | 583 | if tmp.shape[0] > 0 and tmp.shape[1] > 0 or tmp.shape[0] == 0 and tmp.shape[1] == 0: 584 | tempimg[:, :, :, k] = cv2.resize( 585 | tmp, (48, 48), interpolation=cv2.INTER_AREA) 586 | else: 587 | return np.empty(shape=(0,)), np.empty(shape=(0,)), np.empty(shape=(0,)) 588 | 589 | tempimg = (tempimg - 127.5) * 0.0078125 590 | tempimg1 = np.transpose(tempimg, (3, 1, 0, 2)) 591 | 592 | out = self.__onet.feed(tempimg1) 593 | out0 = np.transpose(out[0]) 594 | out1 = np.transpose(out[1]) 595 | out2 = np.transpose(out[2]) 596 | fc1 = np.transpose(out[3]) 597 | 598 | score = out2[1, :] 599 | 600 | points = out1 601 | 602 | ipass = np.where(score > self.__steps_threshold[2]) 603 | 604 | points = points[:, ipass[0]] 605 | 606 | total_boxes = np.hstack( 607 | [total_boxes[ipass[0], 0:4].copy(), np.expand_dims(score[ipass].copy(), 1)]) 608 | 609 | mv = out0[:, ipass[0]] 610 | 611 | w = total_boxes[:, 2] - total_boxes[:, 0] + 1 612 | h = total_boxes[:, 3] - total_boxes[:, 1] + 1 613 | 614 | points[0:5, :] = np.tile(w, (5, 1)) * points[0:5, :] + \ 615 | np.tile(total_boxes[:, 0], (5, 1)) - 1 616 | points[5:10, :] = np.tile( 617 | h, (5, 1)) * points[5:10, :] + np.tile(total_boxes[:, 1], (5, 1)) - 1 618 | 619 | if total_boxes.shape[0] > 0: 620 | total_boxes = self.__bbreg(total_boxes.copy(), np.transpose(mv)) 621 | pick = self.__nms(total_boxes.copy(), 0.7, 'Min') 622 | total_boxes = total_boxes[pick, :] 623 | points = points[:, pick] 624 | fc1 = fc1[:, pick] 625 | fc1 = np.ravel(fc1) 626 | 627 | return total_boxes, points, fc1 628 | 629 | def __del__(self): 630 | self.__session.close() 631 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "4cf638dc157be494602066ba9a25481f0f715a2a0d68610fa367217d9f82c72a" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "absl-py": { 20 | "hashes": [ 21 | "sha256:d9129186431e150d7fe455f1cb1ecbb92bb5dba9da9bc3ef7b012d98c4db2526" 22 | ], 23 | "version": "==0.8.1" 24 | }, 25 | "astor": { 26 | "hashes": [ 27 | "sha256:0e41295809baf43ae8303350e031aff81ae52189b6f881f36d623fa8b2f1960e", 28 | "sha256:37a6eed8b371f1228db08234ed7f6cfdc7817a3ed3824797e20cbb11dc2a7862" 29 | ], 30 | "version": "==0.8.0" 31 | }, 32 | "blinker": { 33 | "hashes": [ 34 | "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6" 35 | ], 36 | "index": "pypi", 37 | "version": "==1.4" 38 | }, 39 | "boto3": { 40 | "hashes": [ 41 | "sha256:839285fbd6f3ab16170af449ae9e33d0eccf97ca22de17d9ff68b8da2310ea06", 42 | "sha256:d93f1774c4bc66e02acdda2067291acb9e228a035435753cb75f83ad2904cbe3" 43 | ], 44 | "index": "pypi", 45 | "version": "==1.9.253" 46 | }, 47 | "botocore": { 48 | "hashes": [ 49 | "sha256:3baf129118575602ada9926f5166d82d02273c250d0feb313fc270944b27c48b", 50 | "sha256:dc080aed4f9b220a9e916ca29ca97a9d37e8e1d296fe89cbaeef929bf0c8066b" 51 | ], 52 | "version": "==1.12.253" 53 | }, 54 | "certifi": { 55 | "hashes": [ 56 | "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", 57 | "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" 58 | ], 59 | "version": "==2019.9.11" 60 | }, 61 | "chardet": { 62 | "hashes": [ 63 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 64 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 65 | ], 66 | "version": "==3.0.4" 67 | }, 68 | "click": { 69 | "hashes": [ 70 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 71 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 72 | ], 73 | "version": "==7.0" 74 | }, 75 | "cycler": { 76 | "hashes": [ 77 | "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d", 78 | "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8" 79 | ], 80 | "version": "==0.10.0" 81 | }, 82 | "decorator": { 83 | "hashes": [ 84 | "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", 85 | "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6" 86 | ], 87 | "version": "==4.4.0" 88 | }, 89 | "docutils": { 90 | "hashes": [ 91 | "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", 92 | "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", 93 | "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" 94 | ], 95 | "version": "==0.15.2" 96 | }, 97 | "flask": { 98 | "hashes": [ 99 | "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", 100 | "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" 101 | ], 102 | "index": "pypi", 103 | "version": "==1.1.1" 104 | }, 105 | "flask-json": { 106 | "hashes": [ 107 | "sha256:470835a9df80c283e3cc0a82fef376ba1be712f711ec385332b3e045a7cc91a8", 108 | "sha256:6bf46b1ebc68f3a085ee955e43c872c299f05cc97d681d88cf3178b816fd200a" 109 | ], 110 | "index": "pypi", 111 | "version": "==0.3.4" 112 | }, 113 | "gast": { 114 | "hashes": [ 115 | "sha256:fe939df4583692f0512161ec1c880e0a10e71e6a232da045ab8edd3756fbadf0" 116 | ], 117 | "version": "==0.2.2" 118 | }, 119 | "gevent": { 120 | "hashes": [ 121 | "sha256:0774babec518a24d9a7231d4e689931f31b332c4517a771e532002614e270a64", 122 | "sha256:0e1e5b73a445fe82d40907322e1e0eec6a6745ca3cea19291c6f9f50117bb7ea", 123 | "sha256:0ff2b70e8e338cf13bedf146b8c29d475e2a544b5d1fe14045aee827c073842c", 124 | "sha256:107f4232db2172f7e8429ed7779c10f2ed16616d75ffbe77e0e0c3fcdeb51a51", 125 | "sha256:14b4d06d19d39a440e72253f77067d27209c67e7611e352f79fe69e0f618f76e", 126 | "sha256:1b7d3a285978b27b469c0ff5fb5a72bcd69f4306dbbf22d7997d83209a8ba917", 127 | "sha256:1eb7fa3b9bd9174dfe9c3b59b7a09b768ecd496debfc4976a9530a3e15c990d1", 128 | "sha256:2711e69788ddb34c059a30186e05c55a6b611cb9e34ac343e69cf3264d42fe1c", 129 | "sha256:28a0c5417b464562ab9842dd1fb0cc1524e60494641d973206ec24d6ec5f6909", 130 | "sha256:3249011d13d0c63bea72d91cec23a9cf18c25f91d1f115121e5c9113d753fa12", 131 | "sha256:44089ed06a962a3a70e96353c981d628b2d4a2f2a75ea5d90f916a62d22af2e8", 132 | "sha256:4bfa291e3c931ff3c99a349d8857605dca029de61d74c6bb82bd46373959c942", 133 | "sha256:50024a1ee2cf04645535c5ebaeaa0a60c5ef32e262da981f4be0546b26791950", 134 | "sha256:53b72385857e04e7faca13c613c07cab411480822ac658d97fd8a4ddbaf715c8", 135 | "sha256:74b7528f901f39c39cdbb50cdf08f1a2351725d9aebaef212a29abfbb06895ee", 136 | "sha256:7d0809e2991c9784eceeadef01c27ee6a33ca09ebba6154317a257353e3af922", 137 | "sha256:896b2b80931d6b13b5d9feba3d4eebc67d5e6ec54f0cf3339d08487d55d93b0e", 138 | "sha256:8d9ec51cc06580f8c21b41fd3f2b3465197ba5b23c00eb7d422b7ae0380510b0", 139 | "sha256:9f7a1e96fec45f70ad364e46de32ccacab4d80de238bd3c2edd036867ccd48ad", 140 | "sha256:ab4dc33ef0e26dc627559786a4fba0c2227f125db85d970abbf85b77506b3f51", 141 | "sha256:d1e6d1f156e999edab069d79d890859806b555ce4e4da5b6418616322f0a3df1", 142 | "sha256:d752bcf1b98174780e2317ada12013d612f05116456133a6acf3e17d43b71f05", 143 | "sha256:e5bcc4270671936349249d26140c267397b7b4b1381f5ec8b13c53c5b53ab6e1" 144 | ], 145 | "index": "pypi", 146 | "version": "==1.4.0" 147 | }, 148 | "google-pasta": { 149 | "hashes": [ 150 | "sha256:40b4f55ba7b44823eac96d055000572c84ce48cacb3e91c100869844064b2d07", 151 | "sha256:79d1ce28b381d68e98ef7707d19909adb58912f8dae8734402454424fc76b8fe", 152 | "sha256:7ca8afc4cfeebf4a079cdf586333d5447cecd19a997475136138fc83c3351bc4" 153 | ], 154 | "version": "==0.1.7" 155 | }, 156 | "greenlet": { 157 | "hashes": [ 158 | "sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0", 159 | "sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28", 160 | "sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8", 161 | "sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304", 162 | "sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0", 163 | "sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214", 164 | "sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043", 165 | "sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6", 166 | "sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625", 167 | "sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc", 168 | "sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638", 169 | "sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163", 170 | "sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4", 171 | "sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490", 172 | "sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248", 173 | "sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939", 174 | "sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87", 175 | "sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720", 176 | "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656" 177 | ], 178 | "markers": "platform_python_implementation == 'CPython'", 179 | "version": "==0.4.15" 180 | }, 181 | "grpcio": { 182 | "hashes": [ 183 | "sha256:0302331e014fc4bac028b6ad480b33f7abfe20b9bdcca7be417124dda8f22115", 184 | "sha256:0aa0cce9c5eb1261b32173a20ed42b51308d55ce28ecc2021e868b3cb90d9503", 185 | "sha256:0c83947575300499adbc308e986d754e7f629be0bdd9bea1ffdd5cf76e1f1eff", 186 | "sha256:0ca26ff968d45efd4ef73447c4d4b34322ea8c7d06fbb6907ce9e5db78f1bbcb", 187 | "sha256:0cf80a7955760c2498f8821880242bb657d70998065ff0d2a082de5ffce230a7", 188 | "sha256:0d40706e57d9833fe0e023a08b468f33940e8909affa12547874216d36bba208", 189 | "sha256:11872069156de34c6f3f9a1deb46cc88bc35dfde88262c4c73eb22b39b16fc55", 190 | "sha256:16065227faae0ab0abf1789bfb92a2cd2ab5da87630663f93f8178026da40e0d", 191 | "sha256:1e33778277685f6fabb22539136269c87c029e39b6321ef1a639b756a1c0a408", 192 | "sha256:2b16be15b1ae656bc7a36642b8c7045be2dde2048bb4b67478003e9d9db8022a", 193 | "sha256:3701dfca3ada27ceef0d17f728ce9dfef155ed20c57979c2b05083082258c6c1", 194 | "sha256:41912ecaf482abf2de74c69f509878f99223f5dd6b2de1a09c955afd4de3cf9b", 195 | "sha256:4332cbd20544fe7406910137590f38b5b3a1f6170258e038652cf478c639430f", 196 | "sha256:44068ecbdc6467c2bff4d8198816c8a2701b6dd1ec16078fceb6adc7c1f577d6", 197 | "sha256:53115960e37059420e2d16a4b04b00dd2ab3b6c3c67babd01ffbfdcd7881a69b", 198 | "sha256:6e7027bcd4070414751e2a5e60706facb98a1fc636497c9bac5442fe37b8ae6b", 199 | "sha256:6ff57fb2f07b7226b5bec89e8e921ea9bd220f35f11e094f2ba38f09eecd49c6", 200 | "sha256:73240e244d7644654bbda1f309f4911748b6a1804b7a8897ddbe8a04c90f7407", 201 | "sha256:785234bbc469bc75e26c868789a2080ffb30bd6e93930167797729889ad06b0b", 202 | "sha256:82f9d3c7f91d2d1885631335c003c5d45ae1cd69cc0bc4893f21fef50b8151bc", 203 | "sha256:86bdc2a965510658407a1372eb61f0c92f763fdfb2795e4d038944da4320c950", 204 | "sha256:95e925b56676a55e6282b3de80a1cbad5774072159779c61eac02791dface049", 205 | "sha256:96673bb4f14bd3263613526d1e7e33fdb38a9130e3ce87bf52314965706e1900", 206 | "sha256:970014205e76920484679035b6fb4b16e02fc977e5aac4d22025da849c79dab9", 207 | "sha256:ace5e8bf11a1571f855f5dab38a9bd34109b6c9bc2864abf24a597598c7e3695", 208 | "sha256:ad375f03eb3b9cb75a24d91eab8609e134d34605f199efc41e20dd642bdac855", 209 | "sha256:b819c4c7dcf0de76788ce5f95daad6d4e753d6da2b6a5f84e5bb5b5ce95fddc4", 210 | "sha256:c17943fd340cbd906db49f3f03c7545e5a66b617e8348b2c7a0d2c759d216af1", 211 | "sha256:d21247150dea86dabd3b628d8bc4b563036db3d332b3f4db3c5b1b0b122cb4f6", 212 | "sha256:d4d500a7221116de9767229ff5dd10db91f789448d85befb0adf5a37b0cd83b5", 213 | "sha256:e2a942a3cfccbbca21a90c144867112698ef36486345c285da9e98c466f22b22", 214 | "sha256:e983273dca91cb8a5043bc88322eb48e2b8d4e4998ff441a1ee79ced89db3909" 215 | ], 216 | "version": "==1.24.1" 217 | }, 218 | "gunicorn": { 219 | "hashes": [ 220 | "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", 221 | "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" 222 | ], 223 | "index": "pypi", 224 | "version": "==19.9.0" 225 | }, 226 | "h5py": { 227 | "hashes": [ 228 | "sha256:063947eaed5f271679ed4ffa36bb96f57bc14f44dd4336a827d9a02702e6ce6b", 229 | "sha256:13c87efa24768a5e24e360a40e0bc4c49bcb7ce1bb13a3a7f9902cec302ccd36", 230 | "sha256:16ead3c57141101e3296ebeed79c9c143c32bdd0e82a61a2fc67e8e6d493e9d1", 231 | "sha256:3dad1730b6470fad853ef56d755d06bb916ee68a3d8272b3bab0c1ddf83bb99e", 232 | "sha256:51ae56894c6c93159086ffa2c94b5b3388c0400548ab26555c143e7cfa05b8e5", 233 | "sha256:54817b696e87eb9e403e42643305f142cd8b940fe9b3b490bbf98c3b8a894cf4", 234 | "sha256:549ad124df27c056b2e255ea1c44d30fb7a17d17676d03096ad5cd85edb32dc1", 235 | "sha256:6998be619c695910cb0effe5eb15d3a511d3d1a5d217d4bd0bebad1151ec2262", 236 | "sha256:6ef7ab1089e3ef53ca099038f3c0a94d03e3560e6aff0e9d6c64c55fb13fc681", 237 | "sha256:769e141512b54dee14ec76ed354fcacfc7d97fea5a7646b709f7400cf1838630", 238 | "sha256:79b23f47c6524d61f899254f5cd5e486e19868f1823298bc0c29d345c2447172", 239 | "sha256:7be5754a159236e95bd196419485343e2b5875e806fe68919e087b6351f40a70", 240 | "sha256:84412798925dc870ffd7107f045d7659e60f5d46d1c70c700375248bf6bf512d", 241 | "sha256:86868dc07b9cc8cb7627372a2e6636cdc7a53b7e2854ad020c9e9d8a4d3fd0f5", 242 | "sha256:8bb1d2de101f39743f91512a9750fb6c351c032e5cd3204b4487383e34da7f75", 243 | "sha256:a5f82cd4938ff8761d9760af3274acf55afc3c91c649c50ab18fcff5510a14a5", 244 | "sha256:aac4b57097ac29089f179bbc2a6e14102dd210618e94d77ee4831c65f82f17c0", 245 | "sha256:bffbc48331b4a801d2f4b7dac8a72609f0b10e6e516e5c480a3e3241e091c878", 246 | "sha256:c0d4b04bbf96c47b6d360cd06939e72def512b20a18a8547fa4af810258355d5", 247 | "sha256:c54a2c0dd4957776ace7f95879d81582298c5daf89e77fb8bee7378f132951de", 248 | "sha256:cbf28ae4b5af0f05aa6e7551cee304f1d317dbed1eb7ac1d827cee2f1ef97a99", 249 | "sha256:d3c59549f90a891691991c17f8e58c8544060fdf3ccdea267100fa5f561ff62f", 250 | "sha256:d7ae7a0576b06cb8e8a1c265a8bc4b73d05fdee6429bffc9a26a6eb531e79d72", 251 | "sha256:ecf4d0b56ee394a0984de15bceeb97cbe1fe485f1ac205121293fc44dcf3f31f", 252 | "sha256:f0e25bb91e7a02efccb50aba6591d3fe2c725479e34769802fcdd4076abfa917", 253 | "sha256:f23951a53d18398ef1344c186fb04b26163ca6ce449ebd23404b153fd111ded9", 254 | "sha256:ff7d241f866b718e4584fa95f520cb19405220c501bd3a53ee11871ba5166ea2" 255 | ], 256 | "version": "==2.10.0" 257 | }, 258 | "idna": { 259 | "hashes": [ 260 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 261 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 262 | ], 263 | "version": "==2.8" 264 | }, 265 | "imageio": { 266 | "hashes": [ 267 | "sha256:c9763e5c187ecf74091c845626b0bdcc6130a20a0de7a86ae0108e2b5335ed3f", 268 | "sha256:f44eb231b9df485874f2ffd22dfd0c3c711e7de076516b9374edea5c65bc67ae" 269 | ], 270 | "index": "pypi", 271 | "version": "==2.6.1" 272 | }, 273 | "itsdangerous": { 274 | "hashes": [ 275 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 276 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 277 | ], 278 | "version": "==1.1.0" 279 | }, 280 | "jinja2": { 281 | "hashes": [ 282 | "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", 283 | "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" 284 | ], 285 | "version": "==2.10.3" 286 | }, 287 | "jmespath": { 288 | "hashes": [ 289 | "sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6", 290 | "sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c" 291 | ], 292 | "version": "==0.9.4" 293 | }, 294 | "joblib": { 295 | "hashes": [ 296 | "sha256:006108c7576b3eb6c5b27761ddbf188eb6e6347696325ab2027ea1ee9a4b922d", 297 | "sha256:6fcc57aacb4e89451fd449e9412687c51817c3f48662c3d8f38ba3f8a0a193ff" 298 | ], 299 | "index": "pypi", 300 | "version": "==0.14.0" 301 | }, 302 | "keras-applications": { 303 | "hashes": [ 304 | "sha256:5579f9a12bcde9748f4a12233925a59b93b73ae6947409ff34aa2ba258189fe5", 305 | "sha256:df4323692b8c1174af821bf906f1e442e63fa7589bf0f1230a0b6bdc5a810c95" 306 | ], 307 | "version": "==1.0.8" 308 | }, 309 | "keras-preprocessing": { 310 | "hashes": [ 311 | "sha256:44aee5f2c4d80c3b29f208359fcb336df80f293a0bb6b1c738da43ca206656fb", 312 | "sha256:5a8debe01d840de93d49e05ccf1c9b81ae30e210d34dacbcc47aeb3049b528e5" 313 | ], 314 | "version": "==1.1.0" 315 | }, 316 | "kiwisolver": { 317 | "hashes": [ 318 | "sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f", 319 | "sha256:26f4fbd6f5e1dabff70a9ba0d2c4bd30761086454aa30dddc5b52764ee4852b7", 320 | "sha256:3b2378ad387f49cbb328205bda569b9f87288d6bc1bf4cd683c34523a2341efe", 321 | "sha256:400599c0fe58d21522cae0e8b22318e09d9729451b17ee61ba8e1e7c0346565c", 322 | "sha256:47b8cb81a7d18dbaf4fed6a61c3cecdb5adec7b4ac292bddb0d016d57e8507d5", 323 | "sha256:53eaed412477c836e1b9522c19858a8557d6e595077830146182225613b11a75", 324 | "sha256:58e626e1f7dfbb620d08d457325a4cdac65d1809680009f46bf41eaf74ad0187", 325 | "sha256:5a52e1b006bfa5be04fe4debbcdd2688432a9af4b207a3f429c74ad625022641", 326 | "sha256:5c7ca4e449ac9f99b3b9d4693debb1d6d237d1542dd6a56b3305fe8a9620f883", 327 | "sha256:682e54f0ce8f45981878756d7203fd01e188cc6c8b2c5e2cf03675390b4534d5", 328 | "sha256:79bfb2f0bd7cbf9ea256612c9523367e5ec51d7cd616ae20ca2c90f575d839a2", 329 | "sha256:7f4dd50874177d2bb060d74769210f3bce1af87a8c7cf5b37d032ebf94f0aca3", 330 | "sha256:8944a16020c07b682df861207b7e0efcd2f46c7488619cb55f65882279119389", 331 | "sha256:8aa7009437640beb2768bfd06da049bad0df85f47ff18426261acecd1cf00897", 332 | "sha256:939f36f21a8c571686eb491acfffa9c7f1ac345087281b412d63ea39ca14ec4a", 333 | "sha256:9733b7f64bd9f807832d673355f79703f81f0b3e52bfce420fc00d8cb28c6a6c", 334 | "sha256:a02f6c3e229d0b7220bd74600e9351e18bc0c361b05f29adae0d10599ae0e326", 335 | "sha256:a0c0a9f06872330d0dd31b45607197caab3c22777600e88031bfe66799e70bb0", 336 | "sha256:acc4df99308111585121db217681f1ce0eecb48d3a828a2f9bbf9773f4937e9e", 337 | "sha256:b64916959e4ae0ac78af7c3e8cef4becee0c0e9694ad477b4c6b3a536de6a544", 338 | "sha256:d3fcf0819dc3fea58be1fd1ca390851bdb719a549850e708ed858503ff25d995", 339 | "sha256:d52e3b1868a4e8fd18b5cb15055c76820df514e26aa84cc02f593d99fef6707f", 340 | "sha256:db1a5d3cc4ae943d674718d6c47d2d82488ddd94b93b9e12d24aabdbfe48caee", 341 | "sha256:e3a21a720791712ed721c7b95d433e036134de6f18c77dbe96119eaf7aa08004", 342 | "sha256:e8bf074363ce2babeb4764d94f8e65efd22e6a7c74860a4f05a6947afc020ff2", 343 | "sha256:f16814a4a96dc04bf1da7d53ee8d5b1d6decfc1a92a63349bb15d37b6a263dd9", 344 | "sha256:f2b22153870ca5cf2ab9c940d7bc38e8e9089fa0f7e5856ea195e1cf4ff43d5a", 345 | "sha256:f790f8b3dff3d53453de6a7b7ddd173d2e020fb160baff578d578065b108a05f" 346 | ], 347 | "version": "==1.1.0" 348 | }, 349 | "markdown": { 350 | "hashes": [ 351 | "sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", 352 | "sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c" 353 | ], 354 | "version": "==3.1.1" 355 | }, 356 | "markupsafe": { 357 | "hashes": [ 358 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 359 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 360 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 361 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 362 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 363 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 364 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 365 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 366 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 367 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 368 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 369 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 370 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 371 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 372 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 373 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 374 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 375 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 376 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 377 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 378 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 379 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 380 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 381 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 382 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 383 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 384 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 385 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 386 | ], 387 | "version": "==1.1.1" 388 | }, 389 | "matplotlib": { 390 | "hashes": [ 391 | "sha256:1febd22afe1489b13c6749ea059d392c03261b2950d1d45c17e3aed812080c93", 392 | "sha256:31a30d03f39528c79f3a592857be62a08595dec4ac034978ecd0f814fa0eec2d", 393 | "sha256:4442ce720907f67a79d45de9ada47be81ce17e6c2f448b3c64765af93f6829c9", 394 | "sha256:796edbd1182cbffa7e1e7a97f1e141f875a8501ba8dd834269ae3cd45a8c976f", 395 | "sha256:934e6243df7165aad097572abf5b6003c77c9b6c480c3c4de6f2ef1b5fdd4ec0", 396 | "sha256:bab9d848dbf1517bc58d1f486772e99919b19efef5dd8596d4b26f9f5ee08b6b", 397 | "sha256:c1fe1e6cdaa53f11f088b7470c2056c0df7d80ee4858dadf6cbe433fcba4323b", 398 | "sha256:e5b8aeca9276a3a988caebe9f08366ed519fff98f77c6df5b64d7603d0e42e36", 399 | "sha256:ec6bd0a6a58df3628ff269978f4a4b924a0d371ad8ce1f8e2b635b99e482877a" 400 | ], 401 | "version": "==3.1.1" 402 | }, 403 | "networkx": { 404 | "hashes": [ 405 | "sha256:cdfbf698749a5014bf2ed9db4a07a5295df1d3a53bf80bf3cbd61edf9df05fa1", 406 | "sha256:f8f4ff0b6f96e4f9b16af6b84622597b5334bf9cae8cf9b2e42e7985d5c95c64" 407 | ], 408 | "version": "==2.4" 409 | }, 410 | "numpy": { 411 | "hashes": [ 412 | "sha256:0b0dd8f47fb177d00fa6ef2d58783c4f41ad3126b139c91dd2f7c4b3fdf5e9a5", 413 | "sha256:25ffe71f96878e1da7e014467e19e7db90ae7d4e12affbc73101bcf61785214e", 414 | "sha256:26efd7f7d755e6ca966a5c0ac5a930a87dbbaab1c51716ac26a38f42ecc9bc4b", 415 | "sha256:28b1180c758abf34a5c3fea76fcee66a87def1656724c42bb14a6f9717a5bdf7", 416 | "sha256:2e418f0a59473dac424f888dd57e85f77502a593b207809211c76e5396ae4f5c", 417 | "sha256:30c84e3a62cfcb9e3066f25226e131451312a044f1fe2040e69ce792cb7de418", 418 | "sha256:4650d94bb9c947151737ee022b934b7d9a845a7c76e476f3e460f09a0c8c6f39", 419 | "sha256:4dd830a11e8724c9c9379feed1d1be43113f8bcce55f47ea7186d3946769ce26", 420 | "sha256:4f2a2b279efde194877aff1f76cf61c68e840db242a5c7169f1ff0fd59a2b1e2", 421 | "sha256:62d22566b3e3428dfc9ec972014c38ed9a4db4f8969c78f5414012ccd80a149e", 422 | "sha256:669795516d62f38845c7033679c648903200980d68935baaa17ac5c7ae03ae0c", 423 | "sha256:75fcd60d682db3e1f8fbe2b8b0c6761937ad56d01c1dc73edf4ef2748d5b6bc4", 424 | "sha256:9395b0a41e8b7e9a284e3be7060db9d14ad80273841c952c83a5afc241d2bd98", 425 | "sha256:9e37c35fc4e9410093b04a77d11a34c64bf658565e30df7cbe882056088a91c1", 426 | "sha256:a0678793096205a4d784bd99f32803ba8100f639cf3b932dc63b21621390ea7e", 427 | "sha256:b46554ad4dafb2927f88de5a1d207398c5385edbb5c84d30b3ef187c4a3894d8", 428 | "sha256:c867eeccd934920a800f65c6068acdd6b87e80d45cd8c8beefff783b23cdc462", 429 | "sha256:dd0667f5be56fb1b570154c2c0516a528e02d50da121bbbb2cbb0b6f87f59bc2", 430 | "sha256:de2b1c20494bdf47f0160bd88ed05f5e48ae5dc336b8de7cfade71abcc95c0b9", 431 | "sha256:f1df7b2b7740dd777571c732f98adb5aad5450aee32772f1b39249c8a50386f6", 432 | "sha256:ffca69e29079f7880c5392bf675eb8b4146479d976ae1924d01cd92b04cccbcc" 433 | ], 434 | "index": "pypi", 435 | "version": "==1.17.3" 436 | }, 437 | "opencv-python": { 438 | "hashes": [ 439 | "sha256:01505b131dc35f60e99a5da98b77156e37f872ae0ff5596e5e68d526bb572d3c", 440 | "sha256:0478a1037505ddde312806c960a5e8958d2cf7a2885e8f2f5dde74c4028e0b04", 441 | "sha256:17810b89f9ef8e8537e75332acf533e619e26ccadbf1b73f24bf338f2d327ddd", 442 | "sha256:19ad2ea9fb32946761b47b9d6eed51876a8329da127f27788263fecd66651ba0", 443 | "sha256:1a250edb739baf3e7c25d99a2ee252aac4f59a97e0bee39237eaa490fd0281d3", 444 | "sha256:3505468970448f66cd776cb9e179570c87988f94b5cf9bcbc4c2d88bd88bbdf1", 445 | "sha256:4e04a91da157885359f487534433340b2d709927559c80acf62c28167e59be02", 446 | "sha256:5a49cffcdec5e37217672579c3343565926d999642844efa9c6a031ed5f32318", 447 | "sha256:604b2ce3d4a86480ced0813da7fba269b4605ad9fea26cd2144d8077928d4b49", 448 | "sha256:61cbb8fa9565a0480c46028599431ad8f19181a7fac8070a700515fd54cd7377", 449 | "sha256:62d7c6e511c9454f099616315c695d02a584048e1affe034b39160db7a2ae34d", 450 | "sha256:6555272dd9efd412d17cdc1a4f4c2da5753c099d95d9ff01aca54bb9782fb5cf", 451 | "sha256:67d994c6b2b14cb9239e85dc7dfa6c08ef7cf6eb4def80c0af6141dfacc8cbb9", 452 | "sha256:68c9cbe538666c4667523821cc56caee49389bea06bae4c0fc2cd68bd264226a", 453 | "sha256:822ad8f628a9498f569c57d30865f5ef9ee17824cee0a1d456211f742028c135", 454 | "sha256:82d972429eb4fee22c1dc4204af2a2e981f010e5e4f66daea2a6c68381b79184", 455 | "sha256:9128924f5b58269ee221b8cf2d736f31bd3bb0391b92ee8504caadd68c8176a2", 456 | "sha256:9172cf8270572c494d8b2ae12ef87c0f6eed9d132927e614099f76843b0c91d7", 457 | "sha256:952bce4d30a8287b17721ddaad7f115dab268efee8576249ddfede80ec2ce404", 458 | "sha256:a8147718e70b1f170a3d26518e992160137365a4db0ed82a9efd3040f9f660d4", 459 | "sha256:bfdb636a3796ff223460ea0fcfda906b3b54f4bef22ae433a5b67e66fab00b25", 460 | "sha256:c9c3f27867153634e1083390920067008ebaaab78aeb09c4e0274e69746cb2c8", 461 | "sha256:d69be21973d450a4662ae6bd1b3df6b1af030e448d7276380b0d1adf7c8c2ae6", 462 | "sha256:db1479636812a6579a3753b72a6fefaa73190f32bf7b19e483f8bc750cebe1a5", 463 | "sha256:db8313d755962a7dd61e5c22a651e0743208adfdb255c6ec8904ce9cb02940c6", 464 | "sha256:e4625a6b032e7797958aeb630d6e3e91e3896d285020aae612e6d7b342d6dfea", 465 | "sha256:e8397a26966a1290836a52c34b362aabc65a422b9ffabcbbdec1862f023ccab8" 466 | ], 467 | "index": "pypi", 468 | "version": "==4.1.1.26" 469 | }, 470 | "opt-einsum": { 471 | "hashes": [ 472 | "sha256:edfada4b1d0b3b782ace8bc14e80618ff629abf53143e1e6bbf9bd00b11ece77" 473 | ], 474 | "version": "==3.1.0" 475 | }, 476 | "pillow": { 477 | "hashes": [ 478 | "sha256:00fdeb23820f30e43bba78eb9abb00b7a937a655de7760b2e09101d63708b64e", 479 | "sha256:01f948e8220c85eae1aa1a7f8edddcec193918f933fb07aaebe0bfbbcffefbf1", 480 | "sha256:08abf39948d4b5017a137be58f1a52b7101700431f0777bec3d897c3949f74e6", 481 | "sha256:099a61618b145ecb50c6f279666bbc398e189b8bc97544ae32b8fcb49ad6b830", 482 | "sha256:2c1c61546e73de62747e65807d2cc4980c395d4c5600ecb1f47a650c6fa78c79", 483 | "sha256:2ed9c4f694861642401f27dc3cb99772be67cd190e84845c749dae0a06c3bfae", 484 | "sha256:338581b30b908e111be578f0297255f6b57a51358cd16fa0e6f664c9a1f88bff", 485 | "sha256:38c7d48a21cd06fdeee93987147b9b1c55b73b4cfcbf83240568bfbd5adee447", 486 | "sha256:43fd026f613c8e48a25eba1a92f4d2ad7f3903c95d8c33a11611a7717d2ab654", 487 | "sha256:4548236844327a718ce3bb182ab32a16fa2050c61e334e959f554cac052fb0df", 488 | "sha256:5090857876c58885cfa388dc649e5db30aae98a068c26f3fd0ac9d7d9a4d9572", 489 | "sha256:5bbba34f97a26a93f5e8dec469ca4ddd712451418add43da946dbaed7f7a98d2", 490 | "sha256:65a28969a025a0eb4594637b6103201dc4ed2a9508bdab56ac33e43e3081c404", 491 | "sha256:892bb52b70bd5ea9dbbc3ac44f38e84f5a04e9d8b1bff48159d96cb795b81159", 492 | "sha256:8a9becd5cbd5062f973bcd2e7bc79483af310222de112b6541f8af1f93a3cc42", 493 | "sha256:972a7aaeb7c4a2795b52eef52ee991ef040b31009f36deca6207a986607b55f3", 494 | "sha256:97b119c436bfa96a92ac2ca525f7025836d4d4e64b1c9f9eff8dbaf3ff1d86f3", 495 | "sha256:9ba37698e242223f8053cc158f130aee046a96feacbeab65893dbe94f5530118", 496 | "sha256:b1b0e1f626a0f079c0d3696db70132fb1f29aa87c66aecb6501a9b8be64ce9f7", 497 | "sha256:c14c1224fd1a5be2733530d648a316974dbbb3c946913562c6005a76f21ca042", 498 | "sha256:c79a8546c48ae6465189e54e3245a97ddf21161e33ff7eaa42787353417bb2b6", 499 | "sha256:ceb76935ac4ebdf6d7bc845482a4450b284c6ccfb281e34da51d510658ab34d8", 500 | "sha256:e22bffaad04b4d16e1c091baed7f2733fc1ebb91e0c602abf1b6834d17158b1f", 501 | "sha256:ec883b8e44d877bda6f94a36313a1c6063f8b1997aa091628ae2f34c7f97c8d5", 502 | "sha256:f1baa54d50ec031d1a9beb89974108f8f2c0706f49798f4777df879df0e1adb6", 503 | "sha256:f53a5385932cda1e2c862d89460992911a89768c65d176ff8c50cddca4d29bed" 504 | ], 505 | "version": "==6.2.0" 506 | }, 507 | "protobuf": { 508 | "hashes": [ 509 | "sha256:125713564d8cfed7610e52444c9769b8dcb0b55e25cc7841f2290ee7bc86636f", 510 | "sha256:1accdb7a47e51503be64d9a57543964ba674edac103215576399d2d0e34eac77", 511 | "sha256:27003d12d4f68e3cbea9eb67427cab3bfddd47ff90670cb367fcd7a3a89b9657", 512 | "sha256:3264f3c431a631b0b31e9db2ae8c927b79fc1a7b1b06b31e8e5bcf2af91fe896", 513 | "sha256:3c5ab0f5c71ca5af27143e60613729e3488bb45f6d3f143dc918a20af8bab0bf", 514 | "sha256:45dcf8758873e3f69feab075e5f3177270739f146255225474ee0b90429adef6", 515 | "sha256:56a77d61a91186cc5676d8e11b36a5feb513873e4ae88d2ee5cf530d52bbcd3b", 516 | "sha256:5984e4947bbcef5bd849d6244aec507d31786f2dd3344139adc1489fb403b300", 517 | "sha256:6b0441da73796dd00821763bb4119674eaf252776beb50ae3883bed179a60b2a", 518 | "sha256:6f6677c5ade94d4fe75a912926d6796d5c71a2a90c2aeefe0d6f211d75c74789", 519 | "sha256:84a825a9418d7196e2acc48f8746cf1ee75877ed2f30433ab92a133f3eaf8fbe", 520 | "sha256:b842c34fe043ccf78b4a6cf1019d7b80113707d68c88842d061fa2b8fb6ddedc", 521 | "sha256:ca33d2f09dae149a1dcf942d2d825ebb06343b77b437198c9e2ef115cf5d5bc1", 522 | "sha256:db83b5c12c0cd30150bb568e6feb2435c49ce4e68fe2d7b903113f0e221e58fe", 523 | "sha256:f50f3b1c5c1c1334ca7ce9cad5992f098f460ffd6388a3cabad10b66c2006b09", 524 | "sha256:f99f127909731cafb841c52f9216e447d3e4afb99b17bebfad327a75aee206de" 525 | ], 526 | "version": "==3.10.0" 527 | }, 528 | "psutil": { 529 | "hashes": [ 530 | "sha256:028a1ec3c6197eadd11e7b46e8cc2f0720dc18ac6d7aabdb8e8c0d6c9704f000", 531 | "sha256:503e4b20fa9d3342bcf58191bbc20a4a5ef79ca7df8972e6197cc14c5513e73d", 532 | "sha256:863a85c1c0a5103a12c05a35e59d336e1d665747e531256e061213e2e90f63f3", 533 | "sha256:954f782608bfef9ae9f78e660e065bd8ffcfaea780f9f2c8a133bb7cb9e826d7", 534 | "sha256:b6e08f965a305cd84c2d07409bc16fbef4417d67b70c53b299116c5b895e3f45", 535 | "sha256:bc96d437dfbb8865fc8828cf363450001cb04056bbdcdd6fc152c436c8a74c61", 536 | "sha256:cf49178021075d47c61c03c0229ac0c60d5e2830f8cab19e2d88e579b18cdb76", 537 | "sha256:d5350cb66690915d60f8b233180f1e49938756fb2d501c93c44f8fb5b970cc63", 538 | "sha256:eba238cf1989dfff7d483c029acb0ac4fcbfc15de295d682901f0e2497e6781a" 539 | ], 540 | "index": "pypi", 541 | "version": "==5.6.3" 542 | }, 543 | "pyparsing": { 544 | "hashes": [ 545 | "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", 546 | "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" 547 | ], 548 | "version": "==2.4.2" 549 | }, 550 | "python-dateutil": { 551 | "hashes": [ 552 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 553 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 554 | ], 555 | "markers": "python_version >= '2.7'", 556 | "version": "==2.8.0" 557 | }, 558 | "pywavelets": { 559 | "hashes": [ 560 | "sha256:076ca8907001fdfe4205484f719d12b4a0262dfe6652fa1cfc3c5c362d14dc84", 561 | "sha256:18a51b3f9416a2ae6e9a35c4af32cf520dd7895f2b69714f4aa2f4342fca47f9", 562 | "sha256:1a64b40f6acb4ffbaccce0545d7fc641744f95351f62e4c6aaa40549326008c9", 563 | "sha256:35959c041ec014648575085a97b498eafbbaa824f86f6e4a59bfdef8a3fe6308", 564 | "sha256:55e39ec848ceec13c9fa1598253ae9dd5c31d09dfd48059462860d2b908fb224", 565 | "sha256:6162dc0ae04669ea04b4b51420777b9ea2d30b0a9d02901b2a3b4d61d159c2e9", 566 | "sha256:68b5c33741d26c827074b3d8f0251de1c3019bb9567b8d303eb093c822ce28f1", 567 | "sha256:720dbcdd3d91c6dfead79c80bf8b00a1d8aa4e5d551dc528c6d5151e4efc3403", 568 | "sha256:7947e51ca05489b85928af52a34fe67022ab5b81d4ae32a4109a99e883a0635e", 569 | "sha256:79f5b54f9dc353e5ee47f0c3f02bebd2c899d49780633aa771fed43fa20b3149", 570 | "sha256:80b924edbc012ded8aa8b91cb2fd6207fb1a9a3a377beb4049b8a07445cec6f0", 571 | "sha256:889d4c5c5205a9c90118c1980df526857929841df33e4cd1ff1eff77c6817a65", 572 | "sha256:935ff247b8b78bdf77647fee962b1cc208c51a7b229db30b9ba5f6da3e675178", 573 | "sha256:98b2669c5af842a70cfab33a7043fcb5e7535a690a00cd251b44c9be0be418e5", 574 | "sha256:9e2528823ccf5a0a1d23262dfefe5034dce89cd84e4e124dc553dfcdf63ebb92", 575 | "sha256:bc5e87b72371da87c9bebc68e54882aada9c3114e640de180f62d5da95749cd3", 576 | "sha256:be105382961745f88d8196bba5a69ee2c4455d87ad2a2e5d1eed6bd7fda4d3fd", 577 | "sha256:c06d2e340c7bf8b9ec71da2284beab8519a3908eab031f4ea126e8ccfc3fd567", 578 | "sha256:cfe79844526dd92e3ecc9490b5031fca5f8ab607e1e858feba232b1b788ff0ea", 579 | "sha256:d510aef84d9852653d079c84f2f81a82d5d09815e625f35c95714e7364570ad4", 580 | "sha256:e02a0558e0c2ac8b8bbe6a6ac18c136767ec56b96a321e0dfde2173adfa5a504" 581 | ], 582 | "version": "==1.1.1" 583 | }, 584 | "requests": { 585 | "hashes": [ 586 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 587 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 588 | ], 589 | "index": "pypi", 590 | "version": "==2.22.0" 591 | }, 592 | "s3transfer": { 593 | "hashes": [ 594 | "sha256:6efc926738a3cd576c2a79725fed9afde92378aa5c6a957e3af010cb019fac9d", 595 | "sha256:b780f2411b824cb541dbcd2c713d0cb61c7d1bcadae204cdddda2b35cef493ba" 596 | ], 597 | "version": "==0.2.1" 598 | }, 599 | "scikit-image": { 600 | "hashes": [ 601 | "sha256:00bbc1d6702c6fe6948a3b9ebd05c0d4cfc49e7a058b87f44ca599e75eadecce", 602 | "sha256:084ecd3dc7ef32bc8725a78b2722012d3e8385c446e812dae2792ce2e9111225", 603 | "sha256:2a874c5864fe686780e2863fb4c323f0ebcd2cb4e4b9b650def54e66e6f12ac0", 604 | "sha256:41f6410054011e1b015af658d3916221cb97964d648c92d533a2144ca7657e62", 605 | "sha256:4fbdbe217ac070dd98bd42ad02872f27ab96b4f5e1fdca6952cff5ffe81c2170", 606 | "sha256:655c7ab92ac3e3bdf9bee47d24784e5b4d9abd127f8732df7c8de89f95b254b1", 607 | "sha256:7d7f43f067ccb82aa335cd0af1de606dfa12193b4551baa8c16be0ecb76a49a9", 608 | "sha256:824fe3e6a06c64f264aa94e59687d8bb7523ffc6778dad958d388e993f5aaecd", 609 | "sha256:b819ac00c298c98ff18266756339e5c5a3682d13d280eb8d4b226582455f249d", 610 | "sha256:f1508b1eddd49c816b3bcae14a4d3eb921f2623559cdae87a0d11133b36ee36d" 611 | ], 612 | "index": "pypi", 613 | "version": "==0.16.1" 614 | }, 615 | "scikit-learn": { 616 | "hashes": [ 617 | "sha256:1ac81293d261747c25ea5a0ee8cd2bb1f3b5ba9ec05421a7f9f0feb4eb7c4116", 618 | "sha256:289361cf003d90b007f5066b27fcddc2d71324c82f1c88e316fedacb0dfdd516", 619 | "sha256:3a14d0abd4281fc3fd2149c486c3ec7cedad848b8d5f7b6f61522029d65a29f8", 620 | "sha256:5083a5e50d9d54548e4ada829598ae63a05651dd2bb319f821ffd9e8388384a6", 621 | "sha256:777cdd5c077b7ca9cb381396c81990cf41d2fa8350760d3cad3b4c460a7db644", 622 | "sha256:8bf2ff63da820d09b96b18e88f9625228457bff8df4618f6b087e12442ef9e15", 623 | "sha256:8d319b71c449627d178f21c57614e21747e54bb3fc9602b6f42906c3931aa320", 624 | "sha256:928050b65781fea9542dfe9bfe02d8c4f5530baa8472ec60782ea77347d2c836", 625 | "sha256:92c903613ff50e22aa95d589f9fff5deb6f34e79f7f21f609680087f137bb524", 626 | "sha256:ae322235def5ce8fae645b439e332e6f25d34bb90d6a6c8e261f17eb476457b7", 627 | "sha256:c1cd6b29eb1fd1cc672ac5e4a8be5f6ea936d094a3dc659ada0746d6fac750b1", 628 | "sha256:c41a6e2685d06bcdb0d26533af2540f54884d40db7e48baed6a5bcbf1a7cc642", 629 | "sha256:d07fcb0c0acbc043faa0e7cf4d2037f71193de3fb04fb8ed5c259b089af1cf5c", 630 | "sha256:d146d5443cda0a41f74276e42faf8c7f283fef49e8a853b832885239ef544e05", 631 | "sha256:eb2b7bed0a26ba5ce3700e15938b28a4f4513578d3e54a2156c29df19ac5fd01", 632 | "sha256:eb9b8ebf59eddd8b96366428238ab27d05a19e89c5516ce294abc35cea75d003" 633 | ], 634 | "index": "pypi", 635 | "version": "==0.21.3" 636 | }, 637 | "scipy": { 638 | "hashes": [ 639 | "sha256:0baa64bf42592032f6f6445a07144e355ca876b177f47ad8d0612901c9375bef", 640 | "sha256:243b04730d7223d2b844bda9500310eecc9eda0cba9ceaf0cde1839f8287dfa8", 641 | "sha256:2643cfb46d97b7797d1dbdb6f3c23fe3402904e3c90e6facfe6a9b98d808c1b5", 642 | "sha256:396eb4cdad421f846a1498299474f0a3752921229388f91f60dc3eda55a00488", 643 | "sha256:3ae3692616975d3c10aca6d574d6b4ff95568768d4525f76222fb60f142075b9", 644 | "sha256:435d19f80b4dcf67dc090cc04fde2c5c8a70b3372e64f6a9c58c5b806abfa5a8", 645 | "sha256:46a5e55850cfe02332998b3aef481d33f1efee1960fe6cfee0202c7dd6fc21ab", 646 | "sha256:75b513c462e58eeca82b22fc00f0d1875a37b12913eee9d979233349fce5c8b2", 647 | "sha256:7ccfa44a08226825126c4ef0027aa46a38c928a10f0a8a8483c80dd9f9a0ad44", 648 | "sha256:89dd6a6d329e3f693d1204d5562dd63af0fd7a17854ced17f9cbc37d5b853c8d", 649 | "sha256:a81da2fe32f4eab8b60d56ad43e44d93d392da228a77e229e59b51508a00299c", 650 | "sha256:a9d606d11eb2eec7ef893eb825017fbb6eef1e1d0b98a5b7fc11446ebeb2b9b1", 651 | "sha256:ac37eb652248e2d7cbbfd89619dce5ecfd27d657e714ed049d82f19b162e8d45", 652 | "sha256:cbc0611699e420774e945f6a4e2830f7ca2b3ee3483fca1aa659100049487dd5", 653 | "sha256:d02d813ec9958ed63b390ded463163685af6025cb2e9a226ec2c477df90c6957", 654 | "sha256:dd3b52e00f93fd1c86f2d78243dfb0d02743c94dd1d34ffea10055438e63b99d" 655 | ], 656 | "version": "==1.3.1" 657 | }, 658 | "sentry-sdk": { 659 | "hashes": [ 660 | "sha256:7d8668f082cb1eb9bf1e0d3f8f9bd5796d05d927c1197af226d044ed32b9815f", 661 | "sha256:ff14935cc3053de0650128f124c36f34a4be120b8cc522c149f5cba342c1fd05" 662 | ], 663 | "index": "pypi", 664 | "version": "==0.13.0" 665 | }, 666 | "six": { 667 | "hashes": [ 668 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 669 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 670 | ], 671 | "version": "==1.12.0" 672 | }, 673 | "tensorboard": { 674 | "hashes": [ 675 | "sha256:4cad2c65f6011e51609b463014c014fd7c6ddd9c1263af1d4f18dd97ed88c2bc", 676 | "sha256:612b789386aa1b2c4804e1961273b37f8e4dd97613f98bc90ff0402d24627f50" 677 | ], 678 | "version": "==1.15.0" 679 | }, 680 | "tensorflow": { 681 | "hashes": [ 682 | "sha256:0a01def34c28298970dc83776dd43877fd59e43fddd8e960d01b6eb849ba9938", 683 | "sha256:1afb2e573fe666eb0dd6a45dbb7679de622a318fcc1fc401fb7819068f2d858b", 684 | "sha256:20bbda24b022c7cedd569bb36a6d620b68e0ea37afc91645c918971c72881388", 685 | "sha256:26b86d32f20dba79da30cb87d67a708678cc6c47f6e596c8783433e282e91f00", 686 | "sha256:5910cdde20a9143760d3b49f6d03a6a09ab5221e2f871eb51dbd422b80da07fc", 687 | "sha256:6265f7d86a66754b875e252effa01f7d54aa4464df3b71182d4be8b269b8a148", 688 | "sha256:79fbb4a38c6e6417d5eef8ccae00b16053750c5d61092ec7caabe4cea6870dc2", 689 | "sha256:8e4bb1c361a9b350497741c1d0a556d9592b5ce52c9b073d88dd053c182f3d15", 690 | "sha256:a214d7bbdae4093bab9a4c582bbf9c4464274cc9db99f94bb42b88c5c961a452", 691 | "sha256:af57e0e16adb4d6ccd387954c1d70e34cc4925b74da9135d2b83ca7d3dd9d102", 692 | "sha256:d1694e25887bc0d0c31d6c7d9c92c8bf9e0499085c7e5a9dbaacf675c44027a8" 693 | ], 694 | "index": "pypi", 695 | "version": "==1.15.0" 696 | }, 697 | "tensorflow-estimator": { 698 | "hashes": [ 699 | "sha256:8853bfb7c3c96fbdc80b3d66c37a10af6ccbcd235dc87474764270c02a0f86b9" 700 | ], 701 | "version": "==1.15.1" 702 | }, 703 | "termcolor": { 704 | "hashes": [ 705 | "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" 706 | ], 707 | "version": "==1.1.0" 708 | }, 709 | "urllib3": { 710 | "hashes": [ 711 | "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", 712 | "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" 713 | ], 714 | "markers": "python_version >= '3.4'", 715 | "version": "==1.25.6" 716 | }, 717 | "werkzeug": { 718 | "hashes": [ 719 | "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", 720 | "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" 721 | ], 722 | "version": "==0.16.0" 723 | }, 724 | "wheel": { 725 | "hashes": [ 726 | "sha256:10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646", 727 | "sha256:f4da1763d3becf2e2cd92a14a7c920f0f00eca30fdde9ea992c836685b9faf28" 728 | ], 729 | "markers": "python_version >= '3'", 730 | "version": "==0.33.6" 731 | }, 732 | "wrapt": { 733 | "hashes": [ 734 | "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" 735 | ], 736 | "version": "==1.11.2" 737 | } 738 | }, 739 | "develop": { 740 | "cycler": { 741 | "hashes": [ 742 | "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d", 743 | "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8" 744 | ], 745 | "version": "==0.10.0" 746 | }, 747 | "kiwisolver": { 748 | "hashes": [ 749 | "sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f", 750 | "sha256:26f4fbd6f5e1dabff70a9ba0d2c4bd30761086454aa30dddc5b52764ee4852b7", 751 | "sha256:3b2378ad387f49cbb328205bda569b9f87288d6bc1bf4cd683c34523a2341efe", 752 | "sha256:400599c0fe58d21522cae0e8b22318e09d9729451b17ee61ba8e1e7c0346565c", 753 | "sha256:47b8cb81a7d18dbaf4fed6a61c3cecdb5adec7b4ac292bddb0d016d57e8507d5", 754 | "sha256:53eaed412477c836e1b9522c19858a8557d6e595077830146182225613b11a75", 755 | "sha256:58e626e1f7dfbb620d08d457325a4cdac65d1809680009f46bf41eaf74ad0187", 756 | "sha256:5a52e1b006bfa5be04fe4debbcdd2688432a9af4b207a3f429c74ad625022641", 757 | "sha256:5c7ca4e449ac9f99b3b9d4693debb1d6d237d1542dd6a56b3305fe8a9620f883", 758 | "sha256:682e54f0ce8f45981878756d7203fd01e188cc6c8b2c5e2cf03675390b4534d5", 759 | "sha256:79bfb2f0bd7cbf9ea256612c9523367e5ec51d7cd616ae20ca2c90f575d839a2", 760 | "sha256:7f4dd50874177d2bb060d74769210f3bce1af87a8c7cf5b37d032ebf94f0aca3", 761 | "sha256:8944a16020c07b682df861207b7e0efcd2f46c7488619cb55f65882279119389", 762 | "sha256:8aa7009437640beb2768bfd06da049bad0df85f47ff18426261acecd1cf00897", 763 | "sha256:939f36f21a8c571686eb491acfffa9c7f1ac345087281b412d63ea39ca14ec4a", 764 | "sha256:9733b7f64bd9f807832d673355f79703f81f0b3e52bfce420fc00d8cb28c6a6c", 765 | "sha256:a02f6c3e229d0b7220bd74600e9351e18bc0c361b05f29adae0d10599ae0e326", 766 | "sha256:a0c0a9f06872330d0dd31b45607197caab3c22777600e88031bfe66799e70bb0", 767 | "sha256:acc4df99308111585121db217681f1ce0eecb48d3a828a2f9bbf9773f4937e9e", 768 | "sha256:b64916959e4ae0ac78af7c3e8cef4becee0c0e9694ad477b4c6b3a536de6a544", 769 | "sha256:d3fcf0819dc3fea58be1fd1ca390851bdb719a549850e708ed858503ff25d995", 770 | "sha256:d52e3b1868a4e8fd18b5cb15055c76820df514e26aa84cc02f593d99fef6707f", 771 | "sha256:db1a5d3cc4ae943d674718d6c47d2d82488ddd94b93b9e12d24aabdbfe48caee", 772 | "sha256:e3a21a720791712ed721c7b95d433e036134de6f18c77dbe96119eaf7aa08004", 773 | "sha256:e8bf074363ce2babeb4764d94f8e65efd22e6a7c74860a4f05a6947afc020ff2", 774 | "sha256:f16814a4a96dc04bf1da7d53ee8d5b1d6decfc1a92a63349bb15d37b6a263dd9", 775 | "sha256:f2b22153870ca5cf2ab9c940d7bc38e8e9089fa0f7e5856ea195e1cf4ff43d5a", 776 | "sha256:f790f8b3dff3d53453de6a7b7ddd173d2e020fb160baff578d578065b108a05f" 777 | ], 778 | "version": "==1.1.0" 779 | }, 780 | "matplotlib": { 781 | "hashes": [ 782 | "sha256:1febd22afe1489b13c6749ea059d392c03261b2950d1d45c17e3aed812080c93", 783 | "sha256:31a30d03f39528c79f3a592857be62a08595dec4ac034978ecd0f814fa0eec2d", 784 | "sha256:4442ce720907f67a79d45de9ada47be81ce17e6c2f448b3c64765af93f6829c9", 785 | "sha256:796edbd1182cbffa7e1e7a97f1e141f875a8501ba8dd834269ae3cd45a8c976f", 786 | "sha256:934e6243df7165aad097572abf5b6003c77c9b6c480c3c4de6f2ef1b5fdd4ec0", 787 | "sha256:bab9d848dbf1517bc58d1f486772e99919b19efef5dd8596d4b26f9f5ee08b6b", 788 | "sha256:c1fe1e6cdaa53f11f088b7470c2056c0df7d80ee4858dadf6cbe433fcba4323b", 789 | "sha256:e5b8aeca9276a3a988caebe9f08366ed519fff98f77c6df5b64d7603d0e42e36", 790 | "sha256:ec6bd0a6a58df3628ff269978f4a4b924a0d371ad8ce1f8e2b635b99e482877a" 791 | ], 792 | "version": "==3.1.1" 793 | }, 794 | "numpy": { 795 | "hashes": [ 796 | "sha256:0b0dd8f47fb177d00fa6ef2d58783c4f41ad3126b139c91dd2f7c4b3fdf5e9a5", 797 | "sha256:25ffe71f96878e1da7e014467e19e7db90ae7d4e12affbc73101bcf61785214e", 798 | "sha256:26efd7f7d755e6ca966a5c0ac5a930a87dbbaab1c51716ac26a38f42ecc9bc4b", 799 | "sha256:28b1180c758abf34a5c3fea76fcee66a87def1656724c42bb14a6f9717a5bdf7", 800 | "sha256:2e418f0a59473dac424f888dd57e85f77502a593b207809211c76e5396ae4f5c", 801 | "sha256:30c84e3a62cfcb9e3066f25226e131451312a044f1fe2040e69ce792cb7de418", 802 | "sha256:4650d94bb9c947151737ee022b934b7d9a845a7c76e476f3e460f09a0c8c6f39", 803 | "sha256:4dd830a11e8724c9c9379feed1d1be43113f8bcce55f47ea7186d3946769ce26", 804 | "sha256:4f2a2b279efde194877aff1f76cf61c68e840db242a5c7169f1ff0fd59a2b1e2", 805 | "sha256:62d22566b3e3428dfc9ec972014c38ed9a4db4f8969c78f5414012ccd80a149e", 806 | "sha256:669795516d62f38845c7033679c648903200980d68935baaa17ac5c7ae03ae0c", 807 | "sha256:75fcd60d682db3e1f8fbe2b8b0c6761937ad56d01c1dc73edf4ef2748d5b6bc4", 808 | "sha256:9395b0a41e8b7e9a284e3be7060db9d14ad80273841c952c83a5afc241d2bd98", 809 | "sha256:9e37c35fc4e9410093b04a77d11a34c64bf658565e30df7cbe882056088a91c1", 810 | "sha256:a0678793096205a4d784bd99f32803ba8100f639cf3b932dc63b21621390ea7e", 811 | "sha256:b46554ad4dafb2927f88de5a1d207398c5385edbb5c84d30b3ef187c4a3894d8", 812 | "sha256:c867eeccd934920a800f65c6068acdd6b87e80d45cd8c8beefff783b23cdc462", 813 | "sha256:dd0667f5be56fb1b570154c2c0516a528e02d50da121bbbb2cbb0b6f87f59bc2", 814 | "sha256:de2b1c20494bdf47f0160bd88ed05f5e48ae5dc336b8de7cfade71abcc95c0b9", 815 | "sha256:f1df7b2b7740dd777571c732f98adb5aad5450aee32772f1b39249c8a50386f6", 816 | "sha256:ffca69e29079f7880c5392bf675eb8b4146479d976ae1924d01cd92b04cccbcc" 817 | ], 818 | "index": "pypi", 819 | "version": "==1.17.3" 820 | }, 821 | "pyparsing": { 822 | "hashes": [ 823 | "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", 824 | "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" 825 | ], 826 | "version": "==2.4.2" 827 | }, 828 | "python-dateutil": { 829 | "hashes": [ 830 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 831 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 832 | ], 833 | "markers": "python_version >= '2.7'", 834 | "version": "==2.8.0" 835 | }, 836 | "six": { 837 | "hashes": [ 838 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 839 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 840 | ], 841 | "version": "==1.12.0" 842 | } 843 | } 844 | } 845 | --------------------------------------------------------------------------------