From 334ea5a2a91da853dc6faf7f48aaa12599201218 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Fri, 11 Jan 2019 20:38:36 +0100 Subject: enable celery tasks --- megapixels/app/server/api_task.py | 187 ++++++++++++++++++++++++++++++++ megapixels/app/server/create.py | 5 + megapixels/app/server/tasks/__init__.py | 47 ++++++++ megapixels/app/server/tasks/blur.py | 91 ++++++++++++++++ megapixels/app/server/tasks/sleep.py | 38 +++++++ megapixels/app/settings/app_cfg.py | 6 + site/assets/css/applets.css | 4 +- 7 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 megapixels/app/server/api_task.py create mode 100644 megapixels/app/server/tasks/__init__.py create mode 100644 megapixels/app/server/tasks/blur.py create mode 100644 megapixels/app/server/tasks/sleep.py diff --git a/megapixels/app/server/api_task.py b/megapixels/app/server/api_task.py new file mode 100644 index 00000000..acdb5b7d --- /dev/null +++ b/megapixels/app/server/api_task.py @@ -0,0 +1,187 @@ +import os +import re +import uuid +import time +import dlib +import simplejson as json +import numpy as np +from flask import Blueprint, request, jsonify +from PIL import Image # todo: try to remove PIL dependency + +# from app.models.sql_factory import load_sql_datasets, list_datasets, get_dataset, get_table +# from app.utils.im_utils import pil2np + +from celery.result import AsyncResult +from app.server.tasks import celery + +api_task = Blueprint('api_task', __name__) + +@api_task.route('/') +def index(): + """Dummy index""" + return jsonify({}) + +# from flask import render_template, redirect, url_for, send_from_directory +# from flask import request, make_response, jsonify +# from . import main, utils +from app.server.tasks import task_lookup, list_active_tasks +# from PIL import Image, ImageOps +# import cv2 as cv + +# import imutils + +@api_task.route('//') +def task_status(task_name, task_id): + """Return celery image processing status""" + if task_name in task_lookup: + task = task_lookup[task_name]['task'].AsyncResult(task_id) + # task = AsyncResult(task_id, app=celery) + else: + return jsonify({ + 'state': 'error', + 'percent': 100, + 'message': 'Unknown task' + }) + + # app.logger.info('task state: {}'.format(task.state)) + if task.state == 'PENDING': + response = { + 'state': task.state, + 'percent': 0, + 'message': 'Pending...' + } + elif task.state != 'FAILURE': + response = { + 'state': task.state, + 'percent': task.info.get('percent', 0), + 'uuid': task.info.get('uuid', 0), + 'message': task.info.get('message', '') + } + if 'result' in task.info: + response['result'] = task.info['result'] + else: + # something went wrong in the background job + response = { + 'state': task.state, + 'percent': 100, + 'message': str(task.info), # this is the exception raised + } + return jsonify(response) + +@api_task.route('/upload/sleep', methods=['GET', 'POST']) +def sleep_test(): + async_task = task_lookup['sleep']['task'].apply_async(args=['sleep_test']) + task_url = '/task/{}/{}'.format('sleep', async_task.id) + return jsonify({ + 'result': True, + 'task_url': task_url, + }) + +# @api_task.route('/upload', methods=['POST']) +# def upload(): + +# style = request.form['style'] +# print('style',style) +# if style in task_lookup: +# task = task_lookup[style]['task'] +# print('task',task) +# else: +# return jsonify({ +# 'result': False, +# 'error': 'Unknown task', +# }) + +# file = request.files['user_image'] +# agree = bool(request.form['agree']) +# ext = request.form['ext'] +# if ext is None: +# ext = request.files['ext'] + +# uuid_name = str(uuid.uuid4()) + +# app.logger.info('[+] style: {}'.format(style)) +# app.logger.info('[+] ext: {}'.format(ext)) +# app.logger.info('[+] uuid_name: {}'.format(uuid_name)) +# app.logger.info('[+] agreed: {}'.format(agree)) + +# # convert PNG to JPG +# print('[+] Resizing image') + +# # LOL MaskRCNN needs to be run outside of the Celery Task +# im = Image.open(file.stream).convert('RGB') +# im = ImageOps.fit(im,(512,512)) +# if agree: +# upload_folder = app.config['UPLOADS'] +# else: +# upload_folder = app.config['UPLOADS_PRIVATE'] + +# fpath = os.path.join(upload_folder, uuid_name + '.jpg') + +# # Save image to disk +# print('[+] Save image to {}'.format(fpath)) +# im.save(fpath, 'JPEG', quality=100) +# im_pil_256 = im.resize((256,256)) + +# print('[+] ensure_np...') +# im_np = imx.ensure_np(im_pil_256) +# #print('[+] resize np...') +# #im_np = imutils.resize(im_np,width=256) + +# upload_dir, render_dir, json_dir, upload_uri, render_uri = get_paths(agree) + +# print('[+] Run mrcnn...') +# try: +# result = mask_rcnn.create_segmentations(im_np,concat=True) +# except: +# print('[-] Error. Could not run mask_rcnn') +# result = [] + +# if len(result) > 0: +# result = result[0] + +# # save data, then pass to celery task +# print('[+] Save masks') +# seg_mask = result['seg_mask'] +# fpath_seg_mask = os.path.join(render_dir, uuid_name + '_seg_mask.jpg') +# #cv.imwrite(fpath_seg_mask,cv.cvtColor(seg_mask,cv.COLOR_BGR2RGB)) +# #seg_mask = seg_mask[:,:,::-1] +# seg_mask_pil = imx.ensure_pil(seg_mask) +# seg_mask_pil.save(fpath_seg_mask, 'JPEG', quality=100) + +# im_mask = result['im_mask'] +# fpath_im_mask = os.path.join(render_dir, uuid_name + '_im_mask.jpg') +# #im_mask = im_mask[:,:,::-1] +# im_mask_pil = imx.ensure_pil(im_mask) +# im_mask_pil.save(fpath_im_mask, 'JPEG',quality=100) +# #cv.imwrite(fpath_im_mask,cv.cvtColor(im_mask,cv.COLOR_BGR2RGB)) + +# celery_result = { +# 'score':str(result['score']), +# 'name':str(result['name']), +# 'class_index':str(result['class_index']), +# 'color':str(result['color']), +# 'fp_im_mask':fpath_im_mask, +# 'fp_seg_mask':fpath_seg_mask, +# 'valid':True +# } +# else: +# print('[-] no reults. process background only') +# celery_result = { +# 'score':None, +# 'name':None, +# 'class_index':None, +# 'color':None, +# 'fp_im_mask':None, +# 'fp_seg_mask':None, +# 'valid':False +# } + +# print('[+] Start celery') +# async_task = task.apply_async(args=[uuid_name, agree, celery_result]) +# task_url = url_for('main.task_status', task_name=style, task_id=async_task.id) + +# return jsonify({ +# 'result': True, +# 'task_url': task_url, +# 'uuid': uuid_name +# }) diff --git a/megapixels/app/server/create.py b/megapixels/app/server/create.py index 4b1333b9..f46bb2a0 100644 --- a/megapixels/app/server/create.py +++ b/megapixels/app/server/create.py @@ -2,7 +2,9 @@ from flask import Flask, Blueprint, jsonify, send_from_directory from flask_sqlalchemy import SQLAlchemy from app.models.sql_factory import connection_url, load_sql_datasets +from app.settings import app_cfg as cfg from app.server.api import api +from app.server.api_task import api_task db = SQLAlchemy() @@ -13,11 +15,14 @@ def create_app(script_info=None): app = Flask(__name__, static_folder='static', static_url_path='') app.config['SQLALCHEMY_DATABASE_URI'] = connection_url app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + app.config['CELERY_BROKER_URL'] = cfg.CELERY_BROKER_URL + app.config['CELERY_RESULT_BACKEND'] = cfg.CELERY_RESULT_BACKEND db.init_app(app) datasets = load_sql_datasets(replace=False, base_model=db.Model) app.register_blueprint(api, url_prefix='/api') + app.register_blueprint(api_task, url_prefix='/task') app.add_url_rule('/', 'serve_page', serve_page, methods=['GET']) @app.route('/', methods=['GET']) diff --git a/megapixels/app/server/tasks/__init__.py b/megapixels/app/server/tasks/__init__.py new file mode 100644 index 00000000..bac7309f --- /dev/null +++ b/megapixels/app/server/tasks/__init__.py @@ -0,0 +1,47 @@ +import simplejson as json +from app.settings import app_cfg as cfg +from celery import Celery + +celery = Celery(__name__, backend=cfg.CELERY_RESULT_BACKEND, broker=cfg.CELERY_BROKER_URL) + +from app.server.tasks.sleep import sleep_task +# from app.server.tasks.blur import blur_task + +def list_active_tasks(): + dropdown = {} + for k,v in task_lookup.items(): + if 'active' not in v or v['active'] is not False: + is_default = 'default' in v and v['default'] is True + task = { + 'name': k, + 'title': v['title'], + 'selected': is_default, + } + dropdown[k] = task + return dropdown + +################################################################### +# Add all valid tasks to this lookup. +# Set 'active': False to disable a task +# Set 'default': True to define the default task + +task_lookup = { + 'sleep': { + 'title': 'Sleep Test', + 'task': sleep_task, + 'active': True, + 'default': True, + }, + # 'blur': { + # 'title': 'Blur', + # 'task': blur_task, + # 'active': False, + # }, + # 'task_dull': { + # 'title': 'DullDream V2', + # 'task': task_dull, + # 'active': True, + # 'default': True, + # } +} + diff --git a/megapixels/app/server/tasks/blur.py b/megapixels/app/server/tasks/blur.py new file mode 100644 index 00000000..508de477 --- /dev/null +++ b/megapixels/app/server/tasks/blur.py @@ -0,0 +1,91 @@ +import os +import sys +import time +import datetime +import json +from PIL import Image, ImageFilter +import cv2 as cv +import numpy as np +from . import main, utils +from .. import basemodels +from flask import current_app as app +from .paths import get_paths +celery = basemodels.celery +from celery.utils.log import get_task_logger +celery_logger = get_task_logger(__name__) +import imutils + +@celery.task(bind=True) +def blur_task(self, uuid_name, agree, extra): + """Process image and update during""" + celery_logger.debug('process_image_task, uuid: {}'.format(uuid_name)) + + upload_dir, render_dir, json_dir, upload_uri, render_uri = get_paths(agree) + + files = [] + + im = Image.open(os.path.join(upload_dir, uuid_name + '.jpg')).convert('RGB') + im = im.resize((256,256)) + files.append({ + 'title': 'Original image', + 'fn': upload_uri + uuid_name + '.jpg' + }) + + self.update_state( + state = 'PROCESSING', + meta = { + 'percent': 0.25, + 'message': 'Applying blur', + 'uuid': uuid_name + }) + + im_np = utils.ensure_np(im) + im_blur = cv.blur(im_np, (5,5), 1.0) + im_blur_pil = utils.ensure_pil(im_blur) + + fn = uuid_name + '_blur.jpg' + fpath = os.path.join(render_dir, fn) + im_blur_pil.save(fpath, 'JPEG', quality=95) + + files.append({ + 'title': 'Blurred image', + 'fn': render_uri + uuid_name + '_blur.jpg' + }) + + time.sleep(3) + + self.update_state( + state = 'PROCESSING', + meta = { + 'percent': 0.50, + 'message': 'Sleeping for some reason', + 'uuid': uuid_name + }) + time.sleep(2) + + self.update_state( + state = 'PROCESSING', + meta = { + 'percent': 0.75, + 'message': 'Sleeping some more', + 'uuid': uuid_name + }) + time.sleep(2) + + data = { + 'uuid': uuid_name, + 'date': str(datetime.datetime.now()), + 'files': files + } + + json_path = os.path.join(json_dir, uuid_name + '.json') + with open(json_path, 'w') as json_file: + json.dump(data, json_file) + + celery_logger.debug('ok') + + return { + 'percent': 100, + 'state': 'complete', + 'uuid': uuid_name, + } diff --git a/megapixels/app/server/tasks/sleep.py b/megapixels/app/server/tasks/sleep.py new file mode 100644 index 00000000..9b91cc52 --- /dev/null +++ b/megapixels/app/server/tasks/sleep.py @@ -0,0 +1,38 @@ +import time + +# from .. import basemodels +# celery = basemodels.celery + +from celery.utils.log import get_task_logger +celery_logger = get_task_logger(__name__) + +from app.server.tasks import celery + +import imutils + +@celery.task(bind=True) +def sleep_task(self, uuid_name): + celery_logger.debug('sleep_task'.format(uuid_name)) + msgs = [ + {'msg':'Uploaded OK','time':.1}, + {'msg':'Segmenting Image...','time':2}, + {'msg':'Found: Person, Horse','time':1}, + {'msg':'Creating Pix2Pix','time':2} + ] + for i,m in enumerate(msgs): + percent = int(float(i)/float(len(msgs))*100.0) + self.update_state( + state = 'PROCESSING', + meta = { + 'percent': percent, + 'message': m['msg'], + 'uuid': uuid_name + }) + celery_logger.debug(m['msg']) + time.sleep(m['time']) + + return { + 'percent': 100, + 'state': 'complete', + 'uuid': uuid_name + } diff --git a/megapixels/app/settings/app_cfg.py b/megapixels/app/settings/app_cfg.py index 2d51a607..a8f41819 100644 --- a/megapixels/app/settings/app_cfg.py +++ b/megapixels/app/settings/app_cfg.py @@ -148,3 +148,9 @@ S3_DATASETS_PATH = "v1" # datasets is already in the filename DIR_SITE_PUBLIC = "../site/public" DIR_SITE_CONTENT = "../site/content" DIR_SITE_TEMPLATES = "../site/templates" + +# ----------------------------------------------------------------------------- +# Celery +# ----------------------------------------------------------------------------- +CELERY_BROKER_URL = 'redis://localhost:6379/0' +CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index 9c37354a..b2b3c85e 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -59,8 +59,8 @@ } .img .bbox { position: absolute; - color: rgba(255,0,0,1); - background: rgba(255,0,0,0.05); + color: rgba(255,255,255,1); + background: rgba(255,255,255,255.05); border: 1px solid; } .cta { -- cgit v1.2.3-70-g09d2 From 433ad25335b94876710ea27fc0d0173f951a8440 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 12 Jan 2019 00:16:29 +0100 Subject: sql name search many terms --- megapixels/app/models/sql_factory.py | 6 +- megapixels/app/server/api.py | 23 +++- megapixels/app/server/api_task.py | 244 +++++++++++++---------------------- megapixels/app/server/tasks/blur.py | 4 +- 4 files changed, 119 insertions(+), 158 deletions(-) diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index a580f28e..82e59b22 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -101,10 +101,12 @@ class SqlDataset: def search_name(self, q): table = self.get_table('identity_meta') uuid_table = self.get_table('uuids') + identity_list = table.query.filter(table.fullname.like(q)).order_by(table.fullname.desc()).limit(10) + return identity_list - identity = table.query.filter(table.fullname.like(q)).order_by(table.fullname.desc()).limit(30) + def get_uuids_for_identities(self, identity_list): identities = [] - for row in identity: + for row in identity_list: uuid = uuid_table.query.filter(uuid_table.id == row.image_id).first() identities.append({ 'uuid': uuid.toJSON(), diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 3683d5fd..cc791bb2 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -3,6 +3,7 @@ import re import time import dlib import numpy as np +import operator from flask import Blueprint, request, jsonify from PIL import Image # todo: try to remove PIL dependency @@ -139,14 +140,32 @@ def name_lookup(dataset_name): dataset = get_dataset(dataset_name) q = request.args.get('q') + q = re.sub('[^a-zA-Z ]+', '*', q) + terms = q.split(' ') # print(q) query = { 'q': q, 'timing': time.time() - start, } - results = dataset.search_name(q + '%') if q else None - + if len(terms) == 1: + names = dataset.search_name('%' + term + '%') if term else [] + results = dataset.get_uuids_for_identities(names) + else: + lookup = {} + results_lookup = {} + for i, term in enumerate(terms): + search_term = '%' + term + '%' + names = dataset.search_name() if term else [] + for name in names: + if name.id in lookup: + lookup[name.id] += 1 + else: + lookup[name.id] = 1 + results_lookup[name.id] = name + top_names = [results_lookup[item[0]] for item in sorted(lookup.items(), key=operator.itemgetter(1))][0:30] + results = dataset.get_uuids_for_identities(top_names) + # print(results) return jsonify({ 'query': query, diff --git a/megapixels/app/server/api_task.py b/megapixels/app/server/api_task.py index acdb5b7d..36990997 100644 --- a/megapixels/app/server/api_task.py +++ b/megapixels/app/server/api_task.py @@ -13,175 +13,117 @@ from PIL import Image # todo: try to remove PIL dependency from celery.result import AsyncResult from app.server.tasks import celery +from app.server.tasks import task_lookup, list_active_tasks api_task = Blueprint('api_task', __name__) @api_task.route('/') def index(): - """Dummy index""" - return jsonify({}) + """List active tasks""" + return jsonify(list_active_tasks) # from flask import render_template, redirect, url_for, send_from_directory # from flask import request, make_response, jsonify # from . import main, utils -from app.server.tasks import task_lookup, list_active_tasks # from PIL import Image, ImageOps # import cv2 as cv - # import imutils @api_task.route('//') def task_status(task_name, task_id): - """Return celery image processing status""" - if task_name in task_lookup: - task = task_lookup[task_name]['task'].AsyncResult(task_id) - # task = AsyncResult(task_id, app=celery) - else: - return jsonify({ - 'state': 'error', - 'percent': 100, - 'message': 'Unknown task' - }) - - # app.logger.info('task state: {}'.format(task.state)) - if task.state == 'PENDING': - response = { - 'state': task.state, - 'percent': 0, - 'message': 'Pending...' - } - elif task.state != 'FAILURE': - response = { - 'state': task.state, - 'percent': task.info.get('percent', 0), - 'uuid': task.info.get('uuid', 0), - 'message': task.info.get('message', '') - } - if 'result' in task.info: - response['result'] = task.info['result'] - else: - # something went wrong in the background job - response = { - 'state': task.state, - 'percent': 100, - 'message': str(task.info), # this is the exception raised - } - return jsonify(response) + """Return celery image processing status""" + if task_name in task_lookup: + task = task_lookup[task_name]['task'].AsyncResult(task_id) + # task = AsyncResult(task_id, app=celery) + else: + return jsonify({ + 'state': 'error', + 'percent': 100, + 'message': 'Unknown task' + }) + + # app.logger.info('task state: {}'.format(task.state)) + if task.state == 'PENDING': + response = { + 'state': task.state, + 'percent': 0, + 'message': 'Pending...' + } + elif task.state != 'FAILURE': + response = { + 'state': task.state, + 'percent': task.info.get('percent', 0), + 'uuid': task.info.get('uuid', 0), + 'message': task.info.get('message', '') + } + if 'result' in task.info: + response['result'] = task.info['result'] + else: + # something went wrong in the background job + response = { + 'state': task.state, + 'percent': 100, + 'message': str(task.info), # this is the exception raised + } + return jsonify(response) @api_task.route('/upload/sleep', methods=['GET', 'POST']) def sleep_test(): - async_task = task_lookup['sleep']['task'].apply_async(args=['sleep_test']) - task_url = '/task/{}/{}'.format('sleep', async_task.id) + async_task = task_lookup['sleep']['task'].apply_async(args=['sleep_test']) + task_url = '/task/{}/{}'.format('sleep', async_task.id) + return jsonify({ + 'result': True, + 'task_url': task_url, + }) + +@api_task.route('/upload', methods=['POST']) +def upload(): + style = request.form['style'] + print('style',style) + if style in task_lookup: + task = task_lookup[style]['task'] + print('task',task) + else: return jsonify({ - 'result': True, - 'task_url': task_url, + 'result': False, + 'error': 'Unknown task', }) -# @api_task.route('/upload', methods=['POST']) -# def upload(): - -# style = request.form['style'] -# print('style',style) -# if style in task_lookup: -# task = task_lookup[style]['task'] -# print('task',task) -# else: -# return jsonify({ -# 'result': False, -# 'error': 'Unknown task', -# }) - -# file = request.files['user_image'] -# agree = bool(request.form['agree']) -# ext = request.form['ext'] -# if ext is None: -# ext = request.files['ext'] - -# uuid_name = str(uuid.uuid4()) - -# app.logger.info('[+] style: {}'.format(style)) -# app.logger.info('[+] ext: {}'.format(ext)) -# app.logger.info('[+] uuid_name: {}'.format(uuid_name)) -# app.logger.info('[+] agreed: {}'.format(agree)) - -# # convert PNG to JPG -# print('[+] Resizing image') - -# # LOL MaskRCNN needs to be run outside of the Celery Task -# im = Image.open(file.stream).convert('RGB') -# im = ImageOps.fit(im,(512,512)) -# if agree: -# upload_folder = app.config['UPLOADS'] -# else: -# upload_folder = app.config['UPLOADS_PRIVATE'] - -# fpath = os.path.join(upload_folder, uuid_name + '.jpg') - -# # Save image to disk -# print('[+] Save image to {}'.format(fpath)) -# im.save(fpath, 'JPEG', quality=100) -# im_pil_256 = im.resize((256,256)) - -# print('[+] ensure_np...') -# im_np = imx.ensure_np(im_pil_256) -# #print('[+] resize np...') -# #im_np = imutils.resize(im_np,width=256) - -# upload_dir, render_dir, json_dir, upload_uri, render_uri = get_paths(agree) - -# print('[+] Run mrcnn...') -# try: -# result = mask_rcnn.create_segmentations(im_np,concat=True) -# except: -# print('[-] Error. Could not run mask_rcnn') -# result = [] - -# if len(result) > 0: -# result = result[0] - -# # save data, then pass to celery task -# print('[+] Save masks') -# seg_mask = result['seg_mask'] -# fpath_seg_mask = os.path.join(render_dir, uuid_name + '_seg_mask.jpg') -# #cv.imwrite(fpath_seg_mask,cv.cvtColor(seg_mask,cv.COLOR_BGR2RGB)) -# #seg_mask = seg_mask[:,:,::-1] -# seg_mask_pil = imx.ensure_pil(seg_mask) -# seg_mask_pil.save(fpath_seg_mask, 'JPEG', quality=100) - -# im_mask = result['im_mask'] -# fpath_im_mask = os.path.join(render_dir, uuid_name + '_im_mask.jpg') -# #im_mask = im_mask[:,:,::-1] -# im_mask_pil = imx.ensure_pil(im_mask) -# im_mask_pil.save(fpath_im_mask, 'JPEG',quality=100) -# #cv.imwrite(fpath_im_mask,cv.cvtColor(im_mask,cv.COLOR_BGR2RGB)) - -# celery_result = { -# 'score':str(result['score']), -# 'name':str(result['name']), -# 'class_index':str(result['class_index']), -# 'color':str(result['color']), -# 'fp_im_mask':fpath_im_mask, -# 'fp_seg_mask':fpath_seg_mask, -# 'valid':True -# } -# else: -# print('[-] no reults. process background only') -# celery_result = { -# 'score':None, -# 'name':None, -# 'class_index':None, -# 'color':None, -# 'fp_im_mask':None, -# 'fp_seg_mask':None, -# 'valid':False -# } - -# print('[+] Start celery') -# async_task = task.apply_async(args=[uuid_name, agree, celery_result]) -# task_url = url_for('main.task_status', task_name=style, task_id=async_task.id) - -# return jsonify({ -# 'result': True, -# 'task_url': task_url, -# 'uuid': uuid_name -# }) + file = request.files['user_image'] + ext = request.form['ext'] + if ext is None: + ext = request.files['ext'] + + uuid_name = str(uuid.uuid4()) + + app.logger.info('[+] style: {}'.format(style)) + app.logger.info('[+] ext: {}'.format(ext)) + app.logger.info('[+] uuid_name: {}'.format(uuid_name)) + + # convert PNG to JPG + print('[+] Resizing image') + + # LOL MaskRCNN needs to be run outside of the Celery Task + im = Image.open(file.stream).convert('RGB') + im = ImageOps.fit(im,(512,512)) + + # # Save image to disk + # print('[+] Save image to {}'.format(fpath)) + # im.save(fpath, 'JPEG', quality=100) + # im_pil_256 = im.resize((256,256)) + + print('[+] ensure_np...') + im_np = imx.ensure_np(im_pil_256) + + celery_result = { + } + + print('[+] Start celery') + async_task = task.apply_async(args=[uuid_name, celery_result]) + task_url = '/task/{}/{}'.format(style, async_task.id) + + return jsonify({ + 'result': True, + 'task_url': task_url, + 'uuid': uuid_name + }) diff --git a/megapixels/app/server/tasks/blur.py b/megapixels/app/server/tasks/blur.py index 508de477..ede75e6a 100644 --- a/megapixels/app/server/tasks/blur.py +++ b/megapixels/app/server/tasks/blur.py @@ -16,12 +16,10 @@ celery_logger = get_task_logger(__name__) import imutils @celery.task(bind=True) -def blur_task(self, uuid_name, agree, extra): +def blur_task(self, uuid_name, extra): """Process image and update during""" celery_logger.debug('process_image_task, uuid: {}'.format(uuid_name)) - upload_dir, render_dir, json_dir, upload_uri, render_uri = get_paths(agree) - files = [] im = Image.open(os.path.join(upload_dir, uuid_name + '.jpg')).convert('RGB') -- cgit v1.2.3-70-g09d2 From 4a9ce42814708a85919663c92be3938530d3742e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 12 Jan 2019 00:58:13 +0100 Subject: rewriting sql scripts to correspond to data format --- megapixels/app/models/sql_factory.py | 120 ++++++++++++++++++----------------- megapixels/app/server/api.py | 8 +-- 2 files changed, 66 insertions(+), 62 deletions(-) diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 82e59b22..05984500 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -88,9 +88,9 @@ class SqlDataset: } def get_identity(self, id): - table = self.get_table('identity_meta') + table = self.get_table('identity') # id += 1 - identity = table.query.filter(table.image_id <= id).order_by(table.image_id.desc()).first().toJSON() + identity = table.query.filter(table.record_id <= id).order_by(table.record_id.desc()).first().toJSON() return { 'uuid': self.select('uuids', id), 'identity': identity, @@ -99,17 +99,17 @@ class SqlDataset: } def search_name(self, q): - table = self.get_table('identity_meta') - uuid_table = self.get_table('uuids') + table = self.get_table('identity') identity_list = table.query.filter(table.fullname.like(q)).order_by(table.fullname.desc()).limit(10) return identity_list - def get_uuids_for_identities(self, identity_list): + def get_file_records_for_identities(self, identity_list): identities = [] + file_record_table = self.get_table('file_record') for row in identity_list: - uuid = uuid_table.query.filter(uuid_table.id == row.image_id).first() + file_record = file_record_table.query.filter(file_record_table.id == row.record_id).first() identities.append({ - 'uuid': uuid.toJSON(), + 'file_record': file_record.toJSON(), 'identity': row.toJSON(), }) return identities @@ -120,7 +120,7 @@ class SqlDataset: return None session = Session() # for obj in session.query(table).filter_by(id=id): - # print(table) + # print(table) obj = session.query(table).filter(table.id == id).first() session.close() return obj.toJSON() @@ -128,43 +128,70 @@ class SqlDataset: def get_table(self, type): if type in self.tables: return self.tables[type] - elif type == 'uuids': - self.tables[type] = self.uuid_table() - elif type == 'roi': - self.tables[type] = self.roi_table() - elif type == 'identity_meta': + elif type == 'file_record': + self.tables[type] = self.file_record_table() + elif type == 'identity': self.tables[type] = self.identity_table() - elif type == 'pose': - self.tables[type] = self.pose_table() + elif type == 'face_roi': + self.tables[type] = self.face_roi_table() + elif type == 'face_pose': + self.tables[type] = self.face_pose_table() else: return None return self.tables[type] - # ==> uuids.csv <== - # index,uuid - # 0,f03fd921-2d56-4e83-8115-f658d6a72287 - def uuid_table(self): - class UUID(self.base_model): - __tablename__ = self.name + "_uuid" + # ==> file_record.csv <== + # index,ext,fn,identity_key,sha256,subdir,uuid,identity_index + def file_record_table(self): + class FileRecord(self.base_model): + __tablename__ = self.name + "_file_record" id = Column(Integer, primary_key=True) + ext = Column(String(3, convert_unicode=True), nullable=False) + fn = Column(String(36, convert_unicode=True), nullable=False) + identity_key = Column(String(36, convert_unicode=True), nullable=False) + sha256 = Column(String(36, convert_unicode=True), nullable=False) + subdir = Column(String(36, convert_unicode=True), nullable=False) uuid = Column(String(36, convert_unicode=True), nullable=False) + identity_index = Column(Integer) def toJSON(self): return { 'id': self.id, 'uuid': self.uuid, + 'identity_index': self.identity_index, } - return UUID + return FileRecord - # ==> roi.csv <== + # ==> identity.csv <== + # index,description,gender,images,fullname + # 0,A. J. Cook,Canadian actress,f,1,0 + def identity_table(self): + class Identity(self.base_model): + __tablename__ = self.name + "_identity" + id = Column(Integer, primary_key=True) + description = Column(String(36, convert_unicode=True), nullable=False) + gender = Column(String(1, convert_unicode=True), nullable=False) + images = Column(Integer, nullable=False) + fullname = Column(String(36, convert_unicode=True), nullable=False) + def toJSON(self): + return { + 'id': self.id, + 'fullname': self.fullname, + 'images': self.images, + 'gender': self.gender, + 'description': self.description, + } + return Identity + + # ==> face_roi.csv <== # index,h,image_height,image_index,image_width,w,x,y # 0,0.33000000000000007,250,0,250,0.32999999999999996,0.33666666666666667,0.35 - def roi_table(self): - class ROI(self.base_model): + def face_roi_table(self): + class FaceROI(self.base_model): __tablename__ = self.name + "_roi" id = Column(Integer, primary_key=True) h = Column(Float, nullable=False) image_height = Column(Integer, nullable=False) - image_index = Column(Integer, nullable=False) + record_index = Column(Integer, nullable=False) image_width = Column(Integer, nullable=False) w = Column(Float, nullable=False) x = Column(Float, nullable=False) @@ -172,7 +199,7 @@ class SqlDataset: def toJSON(self): return { 'id': self.id, - 'image_index': self.image_index, + 'record_index': self.record_index, 'image_height': self.image_height, 'image_width': self.image_width, 'w': self.w, @@ -180,48 +207,25 @@ class SqlDataset: 'x': self.x, 'y': self.y, } - return ROI - - # ==> identity.csv <== - # index,fullname,description,gender,images,image_index - # 0,A. J. Cook,Canadian actress,f,1,0 - def identity_table(self): - class Identity(self.base_model): - __tablename__ = self.name + "_identity" - id = Column(Integer, primary_key=True) - fullname = Column(String(36, convert_unicode=True), nullable=False) - description = Column(String(36, convert_unicode=True), nullable=False) - gender = Column(String(1, convert_unicode=True), nullable=False) - images = Column(Integer, nullable=False) - image_id = Column(Integer, nullable=False) - def toJSON(self): - return { - 'id': self.id, - 'image_id': self.image_id, - 'fullname': self.fullname, - 'images': self.images, - 'gender': self.gender, - 'description': self.description, - } - return Identity + return FaceROI - # ==> pose.csv <== - # index,image_index,pitch,roll,yaw + # ==> face_pose.csv <== + # index,record_index,pitch,roll,yaw # 0,0,11.16264458441435,10.415885631337728,22.99719032415318 - def pose_table(self): - class Pose(self.base_model): + def face_pose_table(self): + class FacePose(self.base_model): __tablename__ = self.name + "_pose" id = Column(Integer, primary_key=True) - image_id = Column(Integer, primary_key=True) + record_id = Column(Integer, nullable=False) pitch = Column(Float, nullable=False) roll = Column(Float, nullable=False) yaw = Column(Float, nullable=False) def toJSON(self): return { 'id': self.id, - 'image_id': self.image_id, + 'record_id': self.record_id, 'pitch': self.pitch, 'roll': self.roll, 'yaw': self.yaw, } - return Pose + return FacePose diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index cc791bb2..0af217e3 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -150,21 +150,21 @@ def name_lookup(dataset_name): } if len(terms) == 1: names = dataset.search_name('%' + term + '%') if term else [] - results = dataset.get_uuids_for_identities(names) + results = dataset.get_file_records_for_identities(names) else: lookup = {} results_lookup = {} for i, term in enumerate(terms): search_term = '%' + term + '%' - names = dataset.search_name() if term else [] + names = dataset.search_name(term) if term else [] for name in names: if name.id in lookup: lookup[name.id] += 1 else: lookup[name.id] = 1 results_lookup[name.id] = name - top_names = [results_lookup[item[0]] for item in sorted(lookup.items(), key=operator.itemgetter(1))][0:30] - results = dataset.get_uuids_for_identities(top_names) + top_names = [results_lookup[item[0]] for item in sorted(lookup.items(), key=operator.itemgetter(1))][0:20] + results = dataset.get_file_records_for_identities(top_names) # print(results) return jsonify({ -- cgit v1.2.3-70-g09d2 From 0fa4c5b7d1e16859f282bdda73bb6af4f4f78b6e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 12 Jan 2019 01:33:16 +0100 Subject: commenting --- megapixels/app/models/sql_factory.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 05984500..a89d89bf 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -62,7 +62,7 @@ def load_sql_dataset(path, replace=False, engine=None, base_model=None): df = pd.read_csv(fn) # fix columns that are named "index", a sql reserved word df.reindex_axis(sorted(df.columns), axis=1) - df.columns = sorted(table.__table__.columns).keys() + # df.columns = sorted(table.__table__.columns).keys() df.to_sql(name=table.__tablename__, con=engine, if_exists='replace', index=False) return dataset @@ -82,12 +82,18 @@ class SqlDataset: self.base_model = base_model def describe(self): + """ + List the available SQL tables for a given dataset. + """ return { 'name': self.name, 'tables': list(self.tables.keys()), } def get_identity(self, id): + """ + Get an identity given an ID. + """ table = self.get_table('identity') # id += 1 identity = table.query.filter(table.record_id <= id).order_by(table.record_id.desc()).first().toJSON() @@ -99,11 +105,17 @@ class SqlDataset: } def search_name(self, q): + """ + Find an identity by name. + """ table = self.get_table('identity') identity_list = table.query.filter(table.fullname.like(q)).order_by(table.fullname.desc()).limit(10) return identity_list def get_file_records_for_identities(self, identity_list): + """ + Given a list of identities, map these to file records. + """ identities = [] file_record_table = self.get_table('file_record') for row in identity_list: @@ -115,6 +127,9 @@ class SqlDataset: return identities def select(self, table, id): + """ + Perform a generic select. + """ table = self.get_table(table) if not table: return None @@ -126,6 +141,9 @@ class SqlDataset: return obj.toJSON() def get_table(self, type): + """ + Get one of these memoized, dynamically generated tables. + """ if type in self.tables: return self.tables[type] elif type == 'file_record': -- cgit v1.2.3-70-g09d2 From 59f692719bb6b4163594243e4c11262dc88466b4 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 12 Jan 2019 17:38:56 +0100 Subject: fixing imports --- .eslintrc.js | 3 ++- client/nameSearch/nameSearch.query.js | 4 ++-- megapixels/app/models/sql_factory.py | 4 +++- megapixels/app/server/api.py | 10 ++++++---- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 364bcad6..1d5aed90 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,7 +16,7 @@ module.exports = { } }, "rules": { - "react/prop-types": 1, + "react/prop-types": 0, "react/jsx-uses-vars": 2, "no-underscore-dangle": 0, "comma-dangle": ["error", "only-multiline"], @@ -37,6 +37,7 @@ module.exports = { "object-curly-newline": 0, "class-methods-use-this": 0, "quotes": "off", + "no-console": "off", }, "env": { "browser": true, diff --git a/client/nameSearch/nameSearch.query.js b/client/nameSearch/nameSearch.query.js index 629b7b1d..99c1da84 100644 --- a/client/nameSearch/nameSearch.query.js +++ b/client/nameSearch/nameSearch.query.js @@ -11,8 +11,8 @@ class NameSearchQuery extends Component { handleInput(value) { this.setState({ q: value }) - if (value.strip().length > 1) { - this.props.actions.search(this.props.payload, value.strip()) + if (value.trim().length > 1) { + this.props.actions.search(this.props.payload, value.trim()) } } diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index a89d89bf..5cdaa889 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -62,7 +62,9 @@ def load_sql_dataset(path, replace=False, engine=None, base_model=None): df = pd.read_csv(fn) # fix columns that are named "index", a sql reserved word df.reindex_axis(sorted(df.columns), axis=1) - # df.columns = sorted(table.__table__.columns).keys() + columns = [column.name for column in table.__table__.columns] + # print(columns) + df.columns = sorted(columns) df.to_sql(name=table.__tablename__, con=engine, if_exists='replace', index=False) return dataset diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 0af217e3..5219a8da 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -148,13 +148,15 @@ def name_lookup(dataset_name): 'q': q, 'timing': time.time() - start, } - if len(terms) == 1: - names = dataset.search_name('%' + term + '%') if term else [] + if len(terms) == 0: + results = [] + elif len(terms) == 1: + names = dataset.search_name('%' + terms[0] + '%') if terms[0] else [] results = dataset.get_file_records_for_identities(names) else: lookup = {} results_lookup = {} - for i, term in enumerate(terms): + for i, term in enumerate(terms[0:5]): search_term = '%' + term + '%' names = dataset.search_name(term) if term else [] for name in names: @@ -163,7 +165,7 @@ def name_lookup(dataset_name): else: lookup[name.id] = 1 results_lookup[name.id] = name - top_names = [results_lookup[item[0]] for item in sorted(lookup.items(), key=operator.itemgetter(1))][0:20] + top_names = [results_lookup[item[0]] for item in sorted(lookup.items(), key=operator.itemgetter(1))][0:10] results = dataset.get_file_records_for_identities(top_names) # print(results) -- cgit v1.2.3-70-g09d2 From c3eec5ef62c6aacf4ca8c8056e1f9150dcd31506 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 12 Jan 2019 17:58:51 +0100 Subject: returning results again --- client/nameSearch/nameSearch.result.js | 2 +- megapixels/app/models/sql_factory.py | 15 ++++++++------- megapixels/app/server/api.py | 6 ++++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/client/nameSearch/nameSearch.result.js b/client/nameSearch/nameSearch.result.js index 9e20228c..38c544cc 100644 --- a/client/nameSearch/nameSearch.result.js +++ b/client/nameSearch/nameSearch.result.js @@ -50,7 +50,7 @@ class NameSearchResult extends Component { ) } const els = results.map((result, i) => { - const { uuid } = result.uuid + const { uuid } = result.file_record const { fullname, gender, description, images } = result.identity return (
diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 5cdaa889..eb91fb37 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -62,9 +62,10 @@ def load_sql_dataset(path, replace=False, engine=None, base_model=None): df = pd.read_csv(fn) # fix columns that are named "index", a sql reserved word df.reindex_axis(sorted(df.columns), axis=1) + print(df.columns) columns = [column.name for column in table.__table__.columns] - # print(columns) - df.columns = sorted(columns) + print(columns) + df.columns = columns df.to_sql(name=table.__tablename__, con=engine, if_exists='replace', index=False) return dataset @@ -121,7 +122,7 @@ class SqlDataset: identities = [] file_record_table = self.get_table('file_record') for row in identity_list: - file_record = file_record_table.query.filter(file_record_table.id == row.record_id).first() + file_record = file_record_table.query.filter(file_record_table.identity_id == row.id).first() identities.append({ 'file_record': file_record.toJSON(), 'identity': row.toJSON(), @@ -172,12 +173,12 @@ class SqlDataset: sha256 = Column(String(36, convert_unicode=True), nullable=False) subdir = Column(String(36, convert_unicode=True), nullable=False) uuid = Column(String(36, convert_unicode=True), nullable=False) - identity_index = Column(Integer) + identity_id = Column(Integer) def toJSON(self): return { 'id': self.id, 'uuid': self.uuid, - 'identity_index': self.identity_index, + 'identity_id': self.identity_id, } return FileRecord @@ -211,7 +212,7 @@ class SqlDataset: id = Column(Integer, primary_key=True) h = Column(Float, nullable=False) image_height = Column(Integer, nullable=False) - record_index = Column(Integer, nullable=False) + record_id = Column(Integer, nullable=False) image_width = Column(Integer, nullable=False) w = Column(Float, nullable=False) x = Column(Float, nullable=False) @@ -219,7 +220,7 @@ class SqlDataset: def toJSON(self): return { 'id': self.id, - 'record_index': self.record_index, + 'record_id': self.record_id, 'image_height': self.image_height, 'image_width': self.image_width, 'w': self.w, diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 5219a8da..5f33e84b 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -111,7 +111,8 @@ def upload(dataset_name): dists.append(round(float(_d), 2)) ids.append(_i+1) - results = [ dataset.get_identity(int(_i)) for _i in ids ] + file_records = [ dataset.get_file_record(int(_i)) for _i in ids ] + identities = [ dataset.get_identity(rec.identity_id) for rec in file_records ] # print(distances) # print(ids) @@ -128,7 +129,7 @@ def upload(dataset_name): # print(results) return jsonify({ 'query': query, - 'results': results, + 'results': identities, 'distances': dists, }) @@ -148,6 +149,7 @@ def name_lookup(dataset_name): 'q': q, 'timing': time.time() - start, } + if len(terms) == 0: results = [] elif len(terms) == 1: -- cgit v1.2.3-70-g09d2 From 5fd0aab76caef8aaf7be77843b9c9260f22dfbb7 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 12 Jan 2019 18:37:43 +0100 Subject: add prefix to name search --- megapixels/app/models/sql_factory.py | 21 +++++++---- megapixels/app/server/api.py | 70 ++++++++++++++++++++++++------------ megapixels/app/server/create.py | 15 ++++++++ old/server/run.py | 2 +- 4 files changed, 78 insertions(+), 30 deletions(-) diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index eb91fb37..25c7e784 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -3,7 +3,7 @@ import glob import time import pandas as pd -from sqlalchemy import create_engine, Table, Column, String, Integer, DateTime, Float +from sqlalchemy import create_engine, Table, Column, String, Integer, DateTime, Float, func from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base @@ -112,7 +112,15 @@ class SqlDataset: Find an identity by name. """ table = self.get_table('identity') - identity_list = table.query.filter(table.fullname.like(q)).order_by(table.fullname.desc()).limit(10) + identity_list = table.query.filter(table.fullname.ilike(q)).order_by(table.fullname.desc()).limit(15) + return identity_list + + def search_description(self, q): + """ + Find an identity by description. + """ + table = self.get_table('identity') + identity_list = table.query.filter(table.description.ilike(q)).order_by(table.description.desc()).limit(15) return identity_list def get_file_records_for_identities(self, identity_list): @@ -123,10 +131,11 @@ class SqlDataset: file_record_table = self.get_table('file_record') for row in identity_list: file_record = file_record_table.query.filter(file_record_table.identity_id == row.id).first() - identities.append({ - 'file_record': file_record.toJSON(), - 'identity': row.toJSON(), - }) + if file_record: + identities.append({ + 'file_record': file_record.toJSON(), + 'identity': row.toJSON(), + }) return identities def select(self, table, id): diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 5f33e84b..743e06f4 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -1,3 +1,5 @@ +import logging +import logging.handlers import os import re import time @@ -141,36 +143,58 @@ def name_lookup(dataset_name): dataset = get_dataset(dataset_name) q = request.args.get('q') - q = re.sub('[^a-zA-Z ]+', '*', q) + q = re.sub('[^a-zA-Z. ]+', '*', q) terms = q.split(' ') - # print(q) query = { 'q': q, 'timing': time.time() - start, } - - if len(terms) == 0: - results = [] - elif len(terms) == 1: - names = dataset.search_name('%' + terms[0] + '%') if terms[0] else [] - results = dataset.get_file_records_for_identities(names) - else: - lookup = {} - results_lookup = {} - for i, term in enumerate(terms[0:5]): - search_term = '%' + term + '%' - names = dataset.search_name(term) if term else [] - for name in names: - if name.id in lookup: - lookup[name.id] += 1 - else: - lookup[name.id] = 1 - results_lookup[name.id] = name - top_names = [results_lookup[item[0]] for item in sorted(lookup.items(), key=operator.itemgetter(1))][0:10] - results = dataset.get_file_records_for_identities(top_names) - # print(results) + print(terms) + + if len(terms) == 0: + return jsonify({ 'query': query, 'results': [] }) + + lookup = {} + results_lookup = {} + + names = dataset.search_name(q + '%') + for name in names: + if name.id in lookup: + print(name.fullname) + lookup[name.id] += 4 + else: + print(name.fullname) + lookup[name.id] = 4 + results_lookup[name.id] = name + + for i, term in enumerate(terms[0:5]): + search_term = '%' + term + '%' + names = dataset.search_name(search_term) if len(term) > 0 else [] + descriptions = dataset.search_description(search_term) if len(term) > 0 else [] + for name in names: + if name.id in lookup: + print(name.fullname) + lookup[name.id] += 2 + else: + print(name.fullname) + lookup[name.id] = 2 + results_lookup[name.id] = name + for name in descriptions: + if name.id in lookup: + print(name.fullname) + lookup[name.id] += 1 + else: + print(name.fullname) + lookup[name.id] = 1 + results_lookup[name.id] = name + + sorted_names = sorted(lookup.items(), key=operator.itemgetter(1), reverse=True)[0:10] + top_names = [results_lookup[item[0]] for item in sorted_names] + results = dataset.get_file_records_for_identities(top_names) + + print(results) return jsonify({ 'query': query, 'results': results, diff --git a/megapixels/app/server/create.py b/megapixels/app/server/create.py index f46bb2a0..a1ce56df 100644 --- a/megapixels/app/server/create.py +++ b/megapixels/app/server/create.py @@ -1,3 +1,18 @@ +import logging +import logging.handlers + +logger = logging.getLogger("") +logger.setLevel(logging.DEBUG) +handler = logging.handlers.RotatingFileHandler("flask.log", + maxBytes=3000000, backupCount=2) +formatter = logging.Formatter( + '[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) +logging.getLogger().addHandler(logging.StreamHandler()) + +logging.debug("starting app") + from flask import Flask, Blueprint, jsonify, send_from_directory from flask_sqlalchemy import SQLAlchemy from app.models.sql_factory import connection_url, load_sql_datasets diff --git a/old/server/run.py b/old/server/run.py index c4c3e8d7..ff2d5009 100644 --- a/old/server/run.py +++ b/old/server/run.py @@ -8,5 +8,5 @@ import logging logging.basicConfig(filename='error.log',level=logging.DEBUG) if __name__ == '__main__': - app.run(host='0.0.0.0',debug=False,threaded=False,port=8000) + app.run(host='0.0.0.0', debug=True, threaded=False, port=8000) pass -- cgit v1.2.3-70-g09d2 From 521bc1e5251105f895dd88a38ddc889cbd0e4431 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 12 Jan 2019 18:55:17 +0100 Subject: updating face search API --- megapixels/app/models/sql_factory.py | 26 ++++++++++++++++---------- megapixels/app/server/api.py | 7 ++----- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 25c7e784..a71eabb0 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -62,9 +62,7 @@ def load_sql_dataset(path, replace=False, engine=None, base_model=None): df = pd.read_csv(fn) # fix columns that are named "index", a sql reserved word df.reindex_axis(sorted(df.columns), axis=1) - print(df.columns) columns = [column.name for column in table.__table__.columns] - print(columns) df.columns = columns df.to_sql(name=table.__tablename__, con=engine, if_exists='replace', index=False) return dataset @@ -97,15 +95,23 @@ class SqlDataset: """ Get an identity given an ID. """ - table = self.get_table('identity') # id += 1 - identity = table.query.filter(table.record_id <= id).order_by(table.record_id.desc()).first().toJSON() - return { - 'uuid': self.select('uuids', id), - 'identity': identity, - 'roi': self.select('roi', id), - 'pose': self.select('pose', id), - } + print('fetching {}'.format(id)) + + file_record_table = self.get_table('file_record') + file_record = file_record_table.query.filter(file_record_table.id == id).first() + + identity_table = self.get_table('identity') + identity = identity_table.query.filter(identity_table.id == file_record.identity_id).first() + + if file_record and identity: + return { + 'file_record': file_record.toJSON(), + 'identity': identity.toJSON(), + 'face_roi': self.select('face_roi', id), + 'face_pose': self.select('face_pose', id), + } + return {} def search_name(self, q): """ diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 743e06f4..af3db4d0 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -113,8 +113,7 @@ def upload(dataset_name): dists.append(round(float(_d), 2)) ids.append(_i+1) - file_records = [ dataset.get_file_record(int(_i)) for _i in ids ] - identities = [ dataset.get_identity(rec.identity_id) for rec in file_records ] + identities = [ dataset.get_identity(int(_i)) for _i in ids ] # print(distances) # print(ids) @@ -151,8 +150,6 @@ def name_lookup(dataset_name): 'timing': time.time() - start, } - print(terms) - if len(terms) == 0: return jsonify({ 'query': query, 'results': [] }) @@ -193,7 +190,7 @@ def name_lookup(dataset_name): sorted_names = sorted(lookup.items(), key=operator.itemgetter(1), reverse=True)[0:10] top_names = [results_lookup[item[0]] for item in sorted_names] results = dataset.get_file_records_for_identities(top_names) - + print(results) return jsonify({ 'query': query, -- cgit v1.2.3-70-g09d2 From 384be7d882d1402220b10bd5b2d0037226b41785 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 12 Jan 2019 19:02:19 +0100 Subject: returning face results again --- client/faceSearch/faceSearch.result.js | 4 ++-- megapixels/app/models/sql_factory.py | 22 ++++++++++++---------- megapixels/app/processors/faiss.py | 3 +++ megapixels/app/server/api.py | 1 + site/assets/css/applets.css | 2 +- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index 95534830..c2509033 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -72,8 +72,8 @@ class FaceSearchResult extends Component { } const els = results.map((result, i) => { const distance = distances[i] - const { uuid } = result.uuid - const { x, y, w, h } = result.roi + const { uuid } = result.file_record + const { x, y, w, h } = result.face_roi const { fullname, gender, description, images } = result.identity const bbox = { left: (100 * x) + '%', diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index a71eabb0..5b3cb5a3 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -96,22 +96,24 @@ class SqlDataset: Get an identity given an ID. """ # id += 1 - print('fetching {}'.format(id)) - file_record_table = self.get_table('file_record') file_record = file_record_table.query.filter(file_record_table.id == id).first() + if not file_record: + return None + identity_table = self.get_table('identity') identity = identity_table.query.filter(identity_table.id == file_record.identity_id).first() - if file_record and identity: - return { - 'file_record': file_record.toJSON(), - 'identity': identity.toJSON(), - 'face_roi': self.select('face_roi', id), - 'face_pose': self.select('face_pose', id), - } - return {} + if not identity: + return None + + return { + 'file_record': file_record.toJSON(), + 'identity': identity.toJSON(), + 'face_roi': self.select('face_roi', id), + 'face_pose': self.select('face_pose', id), + } def search_name(self, q): """ diff --git a/megapixels/app/processors/faiss.py b/megapixels/app/processors/faiss.py index 5156ad71..ab067fd0 100644 --- a/megapixels/app/processors/faiss.py +++ b/megapixels/app/processors/faiss.py @@ -30,6 +30,9 @@ def build_faiss_database(name, recipe): vec_fn = os.path.join(cfg.DIR_FAISS_METADATA, name, "vecs.csv") index_fn = os.path.join(cfg.DIR_FAISS_INDEXES, name + ".index") + if not os.path.exists(vec_fn): + return + index = faiss.index_factory(recipe.dim, recipe.factory_type) keys, rows = load_csv_safe(vec_fn) diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index af3db4d0..5f80a0c4 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -114,6 +114,7 @@ def upload(dataset_name): ids.append(_i+1) identities = [ dataset.get_identity(int(_i)) for _i in ids ] + identities = list(filter(None, identities)) # print(distances) # print(ids) diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index b2b3c85e..b64da4b7 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -60,7 +60,7 @@ .img .bbox { position: absolute; color: rgba(255,255,255,1); - background: rgba(255,255,255,255.05); + background: rgba(255,255,255,0.05); border: 1px solid; } .cta { -- cgit v1.2.3-70-g09d2 From 4060915f156dec87a449a10c96d166d474f2d628 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 12 Jan 2019 19:25:36 +0100 Subject: fix faiss, fix off by one --- megapixels/app/processors/faiss.py | 2 +- megapixels/app/server/api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/megapixels/app/processors/faiss.py b/megapixels/app/processors/faiss.py index ab067fd0..0de8ec69 100644 --- a/megapixels/app/processors/faiss.py +++ b/megapixels/app/processors/faiss.py @@ -27,7 +27,7 @@ def build_all_faiss_databases(): build_faiss_database(name, DefaultRecipe()) def build_faiss_database(name, recipe): - vec_fn = os.path.join(cfg.DIR_FAISS_METADATA, name, "vecs.csv") + vec_fn = os.path.join(cfg.DIR_FAISS_METADATA, name, "face_vector.csv") index_fn = os.path.join(cfg.DIR_FAISS_INDEXES, name + ".index") if not os.path.exists(vec_fn): diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 5f80a0c4..48279040 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -111,7 +111,7 @@ def upload(dataset_name): for _d, _i in zip(distances, indexes): if _d <= THRESHOLD: dists.append(round(float(_d), 2)) - ids.append(_i+1) + ids.append(_i) identities = [ dataset.get_identity(int(_i)) for _i in ids ] identities = list(filter(None, identities)) -- cgit v1.2.3-70-g09d2 From bb7efd0af0db8183b5b3f96ac0de1bfd9cd249ae Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 00:04:35 +0100 Subject: face analysis frontend scaffolding --- client/actions.js | 2 + client/applet.js | 3 + client/common/index.js | 2 + client/common/upload.helpers.js | 155 ++++++++++++++++++++++++ client/common/uploadImage.component.js | 46 +++++++ client/faceAnalysis/faceAnalysis.actions.js | 75 ++++++++++++ client/faceAnalysis/faceAnalysis.container.js | 24 ++++ client/faceAnalysis/faceAnalysis.query.js | 82 +++++++++++++ client/faceAnalysis/faceAnalysis.reducer.js | 32 +++++ client/faceAnalysis/faceAnalysis.result.js | 127 ++++++++++++++++++++ client/faceAnalysis/index.js | 5 + client/faceSearch/faceSearch.query.js | 42 ++----- client/faceSearch/upload.js | 154 ------------------------ client/store.js | 2 + client/types.js | 4 + client/util.js | 17 ++- package-lock.json | 167 ++++++++++++++++++-------- package.json | 3 +- 18 files changed, 705 insertions(+), 237 deletions(-) create mode 100644 client/common/upload.helpers.js create mode 100644 client/common/uploadImage.component.js create mode 100644 client/faceAnalysis/faceAnalysis.actions.js create mode 100644 client/faceAnalysis/faceAnalysis.container.js create mode 100644 client/faceAnalysis/faceAnalysis.query.js create mode 100644 client/faceAnalysis/faceAnalysis.reducer.js create mode 100644 client/faceAnalysis/faceAnalysis.result.js create mode 100644 client/faceAnalysis/index.js delete mode 100644 client/faceSearch/upload.js diff --git a/client/actions.js b/client/actions.js index 2be8229d..7007eb76 100644 --- a/client/actions.js +++ b/client/actions.js @@ -1,7 +1,9 @@ +import * as faceAnalysis from './faceAnalysis/faceAnalysis.actions' import * as faceSearch from './faceSearch/faceSearch.actions' import * as nameSearch from './nameSearch/nameSearch.actions' export { + faceAnalysis, faceSearch, nameSearch, } diff --git a/client/applet.js b/client/applet.js index 80d40657..25291401 100644 --- a/client/applet.js +++ b/client/applet.js @@ -1,12 +1,15 @@ import React, { Component } from 'react' import { Container as FaceSearchContainer } from './faceSearch' +import { Container as FaceAnalysisContainer } from './faceAnalysis' import { Container as NameSearchContainer } from './nameSearch' export default class Applet extends Component { render() { // console.log(this.props) switch (this.props.payload.cmd) { + case 'face_analysis': + return case 'face_search': return case 'name_search': diff --git a/client/common/index.js b/client/common/index.js index cfb34b32..cbd3166e 100644 --- a/client/common/index.js +++ b/client/common/index.js @@ -3,6 +3,7 @@ import DetectionBoxes from './detectionBoxes.component' import DetectionList from './detectionList.component' // import Header from './header.component' import Loader from './loader.component' +import UploadImage from './uploadImage.component' import Sidebar from './sidebar.component' import Gate from './gate.component' import Video from './video.component' @@ -12,6 +13,7 @@ import './common.css' export { Sidebar, Loader, + UploadImage, Gate, TableObject, TableArray, diff --git a/client/common/upload.helpers.js b/client/common/upload.helpers.js new file mode 100644 index 00000000..5a041fd4 --- /dev/null +++ b/client/common/upload.helpers.js @@ -0,0 +1,155 @@ +import ExifReader from 'exifreader' + +export const MAX_SIDE = 300 + +function base64ToUint8Array(string, start, finish) { + start = start || 0 + finish = finish || string.length + // atob that shit + const binary = atob(string) + const buffer = new Uint8Array(binary.length) + for (let i = start; i < finish; i++) { + buffer[i] = binary.charCodeAt(i) + } + return buffer +} + +function getOrientation(uri) { + const exif = new ExifReader() + // Split off the base64 data + const base64String = uri.split(',')[1] + // Read off first 128KB, which is all we need to + // get the EXIF data + const arr = base64ToUint8Array(base64String, 0, 2 ** 17) + try { + exif.load(arr.buffer) + return exif.getTagValue('Orientation') + } catch (err) { + return 1 + } +} + +function applyRotation(canvas, ctx, deg) { + const radians = deg * (Math.PI / 180) + if (deg === 90) { + ctx.translate(canvas.width, 0) + } else if (deg === 180) { + ctx.translate(canvas.width, canvas.height) + } else if (deg === 270) { + ctx.translate(0, canvas.height) + } + ctx.rotate(radians) +} + +/** + * Mapping from EXIF orientation values to data + * regarding the rotation and mirroring necessary to + * render the canvas correctly + * Derived from: + * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ + */ +const orientationToTransform = { + 1: { rotation: 0, mirror: false }, + 2: { rotation: 0, mirror: true }, + 3: { rotation: 180, mirror: false }, + 4: { rotation: 180, mirror: true }, + 5: { rotation: 90, mirror: true }, + 6: { rotation: 90, mirror: false }, + 7: { rotation: 270, mirror: true }, + 8: { rotation: 270, mirror: false } +} + +function applyOrientationCorrection(canvas, ctx, uri) { + const orientation = getOrientation(uri) + // Only apply transform if there is some non-normal orientation + if (orientation && orientation !== 1) { + const transform = orientationToTransform[orientation] + const { rotation } = transform + const flipAspect = rotation === 90 || rotation === 270 + if (flipAspect) { + // Fancy schmancy swap algo + canvas.width = canvas.height + canvas.width + canvas.height = canvas.width - canvas.height + canvas.width -= canvas.height + } + if (rotation > 0) { + applyRotation(canvas, ctx, rotation) + } + } +} + +function getScale(width, height, viewportWidth, viewportHeight, fillViewport) { + function fitHorizontal() { + return viewportWidth / width + } + function fitVertical() { + return viewportHeight / height + } + fillViewport = !!fillViewport + const landscape = (width / height) > (viewportWidth / viewportHeight) + if (landscape) { + if (fillViewport) { + return fitVertical() + } + if (width > viewportWidth) { + return fitHorizontal() + } + } else { + if (fillViewport) { + return fitHorizontal() + } + if (height > viewportHeight) { + return fitVertical() + } + } + return 1 +} + +export function renderToCanvas(img, options) { + if (!img) return null + options = options || {} + + // Canvas max size for any side + const maxSize = MAX_SIDE + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + const initialScale = options.scale || 1 + // Scale to needed to constrain canvas to max size + let scale = getScale(img.width * initialScale, img.height * initialScale, maxSize, maxSize, true) + // Still need to apply the user defined scale + scale *= initialScale + canvas.width = Math.round(img.width * scale) + canvas.height = Math.round(img.height * scale) + const { correctOrientation } = options + const jpeg = !!img.src.match(/data:image\/jpeg|\.jpeg$|\.jpg$/i) + const hasDataURI = !!img.src.match(/^data:/) + + ctx.save() + + // Can only correct orientation on JPEGs represented as dataURIs + // for the time being + if (correctOrientation && jpeg && hasDataURI) { + applyOrientationCorrection(canvas, ctx, img.src) + } + // Resize image if too large + if (scale !== 1) { + ctx.scale(scale, scale) + } + + ctx.drawImage(img, 0, 0) + ctx.restore() + + return canvas +} + +export function renderThumbnail(img) { + const resized = renderToCanvas(img, { correctOrientation: true }) + const canvas = document.createElement('canvas') // document.querySelector('#user_photo_canvas') + const ctx = canvas.getContext('2d') + ctx.fillStyle = 'black' + ctx.fillRect(0, 0, MAX_SIDE, MAX_SIDE) + const xOffset = (MAX_SIDE - resized.width) / 2 + const yOffset = (MAX_SIDE - resized.height) / 2 + ctx.drawImage(resized, xOffset, yOffset) + return canvas +} diff --git a/client/common/uploadImage.component.js b/client/common/uploadImage.component.js new file mode 100644 index 00000000..eb8cc60f --- /dev/null +++ b/client/common/uploadImage.component.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react' + +import { renderThumbnail } from './upload.helpers' + +export default class UploadImageComponent extends Component { + upload(e) { + const files = e.dataTransfer ? e.dataTransfer.files : e.target.files + let i + let file + for (i = 0; i < files.length; i++) { + file = files[i] + if (file && file.type.match('image.*')) break + } + if (!file) return + const fr = new FileReader() + fr.onload = fileReaderEvent => { + fr.onload = null + const img = new Image() + img.onload = () => { + img.onload = null + this.resizeAndUpload(img) + } + img.src = fileReaderEvent.result + } + fr.readAsDataURL(files[0]) + } + + resizeAndUpload(img) { + const canvas = renderThumbnail(img) + canvas.toBlob(blob => { + this.props.onUpload(blob) + }, 'image/jpeg', 80) + } + + render() { + return ( + + ) + } +} diff --git a/client/faceAnalysis/faceAnalysis.actions.js b/client/faceAnalysis/faceAnalysis.actions.js new file mode 100644 index 00000000..90d7156f --- /dev/null +++ b/client/faceAnalysis/faceAnalysis.actions.js @@ -0,0 +1,75 @@ +// import fetchJsonp from 'fetch-jsonp' +import * as types from '../types' +// import { hashPath } from '../util' +import { store } from '../store' +import { get, post } from '../util' +// import querystring from 'query-string' + +// urls + +const url = { + upload: (dataset) => process.env.API_HOST + '/api/dataset/' + dataset + '/face', +} +export const publicUrl = { +} + +// standard loading events + +const loading = (tag, offset) => ({ + type: types.faceAnalysis.loading, + tag, + offset +}) +const loaded = (tag, data, offset = 0) => ({ + type: types.faceAnalysis.loaded, + tag, + data, + offset +}) +const error = (tag, err) => ({ + type: types.faceAnalysis.error, + tag, + err +}) + +// search UI functions + +export const updateOptions = opt => dispatch => { + dispatch({ type: types.faceAnalysis.update_options, opt }) +} + +// API functions + +export const upload = (payload, file) => dispatch => { + // const { options } = store.getState().faceAnalysis + const tag = 'result' + const fd = new FormData() + fd.append('query_img', file) + // fd.append('limit', options.perPage) + // if (!query) { + dispatch(loading(tag)) + // } + post(url.upload(payload.dataset), fd) + .then(data => { + dispatch(loaded(tag, data)) + }) + .catch(err => dispatch(error(tag, err))) +} + +// task polling + +const POLL_DELAY = 500 +let pollTimeout = null + +export const poll = (payload, taskURL) => dispatch => { + const tag = 'poll' + clearTimeout(pollTimeout) + dispatch(loading(tag)) + get(taskURL) + .then(data => { + dispatch(loaded(tag, data)) + // check if complete + pollTimeout = setTimeout(() => poll(payload, taskURL), POLL_DELAY) + }) + .catch(err => dispatch(error(tag, err))) +} diff --git a/client/faceAnalysis/faceAnalysis.container.js b/client/faceAnalysis/faceAnalysis.container.js new file mode 100644 index 00000000..a86bcaa4 --- /dev/null +++ b/client/faceAnalysis/faceAnalysis.container.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from './faceAnalysis.actions' + +import FaceAnalysisQuery from './faceAnalysis.query' +import FaceAnalysisResult from './faceAnalysis.result' + +class FaceAnalysisContainer extends Component { + render() { + const { payload } = this.props + // console.log(payload) + return ( +
+ + +
+ ) + } +} + + +export default FaceAnalysisContainer diff --git a/client/faceAnalysis/faceAnalysis.query.js b/client/faceAnalysis/faceAnalysis.query.js new file mode 100644 index 00000000..86dbe1ae --- /dev/null +++ b/client/faceAnalysis/faceAnalysis.query.js @@ -0,0 +1,82 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import { Loader } from '../common' +import * as actions from './faceAnalysis.actions' + +// function parse_bbox(s) { +// // "BBox: (77,86), (166, 177), width:89, height:91" +// try { +// const [x, y, w, h, width, height] = s.replace(/\D/g, ' ').replace(/\s+/g, ' ').trim().split(' ') +// return { x, y, w, h } +// } +// } + +class FaceAnalysisQuery extends Component { + state = { + image: null + } + + upload(blob) { + this.props.actions.upload(this.props.payload, blob) + } + + render() { + const { result } = this.props + const { image } = this.state + console.log(result) + const style = {} + if (image) { + style.backgroundImage = 'url(' + image + ')' + style.backgroundSize = 'cover' + style.opacity = 1 + } + return ( +
+
+
+ {image ? null : } + +
+ {result.loading && ( +
+ +
+ )} +
+
+

Face Analysis

+

+ {'Put yourself under the microscope of various facial recognition algorithms. See what can be determined from a photo.'} +

+
    +
  1. Upload a photo of yourself
  2. +
  3. {'Your search data is never stored and immediately cleared '} + {'once you leave this page.'}
  4. +
+

+ Read more about privacy. +

+
+
+ ) + } +} + +const mapStateToProps = state => ({ + result: state.faceAnalysis.result, + options: state.faceAnalysis.options, +}) + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(FaceAnalysisQuery) diff --git a/client/faceAnalysis/faceAnalysis.reducer.js b/client/faceAnalysis/faceAnalysis.reducer.js new file mode 100644 index 00000000..d8e914ab --- /dev/null +++ b/client/faceAnalysis/faceAnalysis.reducer.js @@ -0,0 +1,32 @@ +import * as types from '../types' + +const initialState = () => ({ + query: {}, + result: {}, + loading: false, +}) + +export default function faceAnalysisReducer(state = initialState(), action) { + switch (action.type) { + case types.faceAnalysis.loading: + return { + ...state, + [action.tag]: { loading: true }, + } + + case types.faceAnalysis.loaded: + return { + ...state, + [action.tag]: action.data, + } + + case types.faceAnalysis.error: + return { + ...state, + [action.tag]: { error: action.err }, + } + + default: + return state + } +} diff --git a/client/faceAnalysis/faceAnalysis.result.js b/client/faceAnalysis/faceAnalysis.result.js new file mode 100644 index 00000000..b825a0cb --- /dev/null +++ b/client/faceAnalysis/faceAnalysis.result.js @@ -0,0 +1,127 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { courtesyS } from '../util' + +import * as actions from './faceAnalysis.actions' +import { Loader } from '../common' + +const errors = { + bbox: ( +
+

No face found

+ {"Sorry, we didn't detect a face in that image. "} + {"Please choose an image where the face is large and clear."} +
+ ), + nomatch: ( +
+

{"You're clear"}

+ {"No images in this dataset match your face. We show only matches above 70% probability."} +
+ ), + error: ( +
+

{"No matches found"}

+ {"Sorry, an error occured."} +
+ ), + bad_dataset: ( +
+

{""}

+ {""} +
+ ), + not_an_image: ( +
+

{"Not an image"}

+ {"Sorry, the file you uploaded was not recognized as an image. Please upload a JPG or PNG image and try again."} +
+ ), +} + +class FaceAnalysisResult extends Component { + render() { + const { dataset } = this.props.payload + const { query, distances, results, loading, error } = this.props.result + console.log(this.props.result) + if (loading) { + return ( +
+
+
+

Searching...

+
+
+ ) + } + if (error) { + // console.log(error) + let errorMessage = errors[error] || errors.error + return ( +
{errorMessage}
+ ) + } + if (!results) { + return
+ } + if (!results.length) { + return ( +
{errors.nomatch}
+ ) + } + const els = results.map((result, i) => { + const distance = distances[i] + const { uuid } = result.file_record + const { x, y, w, h } = result.face_roi + const { fullname, gender, description, images } = result.identity + const bbox = { + left: (100 * x) + '%', + top: (100 * y) + '%', + width: (100 * w) + '%', + height: (100 * h) + '%', + } + // console.log(bbox) + return ( +
+
+ +
+
+ {fullname} {'('}{gender}{')'}
+ {description}
+ {courtesyS(images, 'image')}{' in dataset'}
+ {Math.round((1 - distance) * 100)}{'% match'} +
+ ) + }) + + return ( +
+
+

Did we find you?

+ {'These faces matched images in the '} + {dataset} + {' dataset with over 70% probability.'} +
+ Query took {query.timing.toFixed(2)} seconds +
+
+ {els} +
+
+ ) + } +} + +const mapStateToProps = state => ({ + query: state.faceAnalysis.query, + result: state.faceAnalysis.result, + options: state.faceAnalysis.options, +}) + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(FaceAnalysisResult) diff --git a/client/faceAnalysis/index.js b/client/faceAnalysis/index.js new file mode 100644 index 00000000..efa39ded --- /dev/null +++ b/client/faceAnalysis/index.js @@ -0,0 +1,5 @@ +import Container from './faceAnalysis.container' + +export { + Container, +} diff --git a/client/faceSearch/faceSearch.query.js b/client/faceSearch/faceSearch.query.js index 9f778ca0..2d140813 100644 --- a/client/faceSearch/faceSearch.query.js +++ b/client/faceSearch/faceSearch.query.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' -import { Loader } from '../common' +import { Loader, UploadImage } from '../common' import * as actions from './faceSearch.actions' // function parse_bbox(s) { @@ -18,23 +18,8 @@ class FaceSearchQuery extends Component { image: null } - upload(e) { - const { payload } = this.props - const files = e.dataTransfer ? e.dataTransfer.files : e.target.files - let i - let file - for (i = 0; i < files.length; i++) { - file = files[i] - if (file && file.type.match('image.*')) break - } - if (!file) return - const fr = new FileReader() - fr.onload = () => { - fr.onload = null - this.setState({ image: fr.result }) - } - fr.readAsDataURL(files[0]) - this.props.actions.upload(this.props.payload, file) + upload(blob) { + this.props.actions.upload(this.props.payload, blob) } render() { @@ -50,22 +35,15 @@ class FaceSearchQuery extends Component { return (
- {result.loading ? -
- -
- :
+
{image ? null : } - - +
- } + {result.loading && ( +
+ +
+ )}

Search by Image

diff --git a/client/faceSearch/upload.js b/client/faceSearch/upload.js deleted file mode 100644 index f18bdce6..00000000 --- a/client/faceSearch/upload.js +++ /dev/null @@ -1,154 +0,0 @@ -const MAX_SIDE = 300 - -function render(img){ - var resized = renderToCanvas(img, { correctOrientation: true }) - var canvas = document.createElement('canvas') // document.querySelector('#user_photo_canvas') - ctx = canvas.getContext('2d') - ctx.fillStyle = 'black' - ctx.fillRect(0, 0, MAX_SIDE, MAX_SIDE) - var x_offset = (MAX_SIDE - resized.width) / 2 - var y_offset = (MAX_SIDE - resized.height) / 2 - ctx.drawImage(resized, x_offset, y_offset) - return canvas -} -function renderToCanvas(img, options) { - if (!img) return - options = options || {} - - // Canvas max size for any side - var maxSize = MAX_SIDE - var canvas = document.createElement('canvas') - var ctx = canvas.getContext('2d') - var initialScale = options.scale || 1 - // Scale to needed to constrain canvas to max size - var scale = getScale(img.width * initialScale, img.height * initialScale, maxSize, maxSize, true) - // Still need to apply the user defined scale - scale *= initialScale - var width = canvas.width = Math.round(img.width * scale) - var height = canvas.height = Math.round(img.height * scale) - var correctOrientation = options.correctOrientation - var jpeg = !!img.src.match(/data:image\/jpeg|\.jpeg$|\.jpg$/i) - var hasDataURI = !!img.src.match(/^data:/) - - ctx.save() - - // Can only correct orientation on JPEGs represented as dataURIs - // for the time being - if (correctOrientation && jpeg && hasDataURI) { - applyOrientationCorrection(canvas, ctx, img.src) - } - // Resize image if too large - if (scale !== 1) { - ctx.scale(scale, scale) - } - - ctx.drawImage(img, 0, 0) - ctx.restore() - - return canvas -} - -function getScale(width, height, viewportWidth, viewportHeight, fillViewport) { - fillViewport = !!fillViewport - var landscape = (width / height) > (viewportWidth / viewportHeight) - if (landscape) { - if (fillViewport) { - return fitVertical() - } else if (width > viewportWidth) { - return fitHorizontal() - } - } else { - if (fillViewport) { - return fitHorizontal() - } else if (height > viewportHeight) { - return fitVertical() - } - } - return 1 - - function fitHorizontal() { - return viewportWidth / width - } - - function fitVertical() { - return viewportHeight / height - } -} - -function applyOrientationCorrection(canvas, ctx, uri) { - var orientation = getOrientation(uri) - // Only apply transform if there is some non-normal orientation - if (orientation && orientation !== 1) { - var transform = orientationToTransform[orientation] - var rotation = transform.rotation - var mirror = transform.mirror - var flipAspect = rotation === 90 || rotation === 270 - if (flipAspect) { - // Fancy schmancy swap algo - canvas.width = canvas.height + canvas.width - canvas.height = canvas.width - canvas.height - canvas.width -= canvas.height - } - if (rotation > 0) { - applyRotation(canvas, ctx, rotation) - } - } -} - -function applyRotation(canvas, ctx, deg) { - var radians = deg * (Math.PI / 180) - if (deg === 90) { - ctx.translate(canvas.width, 0) - } else if (deg === 180) { - ctx.translate(canvas.width, canvas.height) - } else if (deg == 270) { - ctx.translate(0, canvas.height) - } - ctx.rotate(radians) -} - -function getOrientation (uri) { - var exif = new ExifReader - // Split off the base64 data - var base64String = uri.split(',')[1] - // Read off first 128KB, which is all we need to - // get the EXIF data - var arr = base64ToUint8Array(base64String, 0, Math.pow(2, 17)) - try { - exif.load(arr.buffer) - return exif.getTagValue('Orientation') - } catch (err) { - return 1 - } -} - -function base64ToUint8Array(string, start, finish) { - var start = start || 0 - var finish = finish || string.length - // atob that shit - var binary = atob(string) - var buffer = new Uint8Array(binary.length) - for (var i = start; i < finish; i++) { - buffer[i] = binary.charCodeAt(i) - } - return buffer -} - -/** - * Mapping from EXIF orientation values to data - * regarding the rotation and mirroring necessary to - * render the canvas correctly - * Derived from: - * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ - */ -var orientationToTransform = { - 1: { rotation: 0, mirror: false }, - 2: { rotation: 0, mirror: true }, - 3: { rotation: 180, mirror: false }, - 4: { rotation: 180, mirror: true }, - 5: { rotation: 90, mirror: true }, - 6: { rotation: 90, mirror: false }, - 7: { rotation: 270, mirror: true }, - 8: { rotation: 270, mirror: false } -} - diff --git a/client/store.js b/client/store.js index 13612f2d..e896bc58 100644 --- a/client/store.js +++ b/client/store.js @@ -1,10 +1,12 @@ import { applyMiddleware, compose, combineReducers, createStore } from 'redux' import thunk from 'redux-thunk' +import faceAnalysisReducer from './faceAnalysis/faceAnalysis.reducer' import faceSearchReducer from './faceSearch/faceSearch.reducer' import nameSearchReducer from './nameSearch/nameSearch.reducer' const rootReducer = combineReducers({ + faceAnalysis: faceAnalysisReducer, faceSearch: faceSearchReducer, nameSearch: nameSearchReducer, }) diff --git a/client/types.js b/client/types.js index fb1fbe30..2d35ec36 100644 --- a/client/types.js +++ b/client/types.js @@ -6,6 +6,10 @@ export const tagAsType = (type, names) => ( }, {}) ) +export const faceAnalysis = tagAsType('faceAnalysis', [ + 'loading', 'loaded', 'error', 'update_options', +]) + export const faceSearch = tagAsType('faceSearch', [ 'loading', 'loaded', 'error', 'update_options', ]) diff --git a/client/util.js b/client/util.js index f181ad0f..d0db0d98 100644 --- a/client/util.js +++ b/client/util.js @@ -82,6 +82,21 @@ export const preloadImage = opt => { /* AJAX */ +export const get = (uri, data) => { + let headers = { + Accept: 'application/json, application/xml, text/play, text/html, *.*', + } + let opt = { + method: 'GET', + body: data, + headers, + // credentials: 'include', + } + // console.log(headers) + // headers['X-CSRFToken'] = csrftoken + return fetch(uri, opt).then(res => res.json()) +} + export const post = (uri, data) => { let headers if (data instanceof FormData) { @@ -99,7 +114,7 @@ export const post = (uri, data) => { method: 'POST', body: data, headers, - credentials: 'include', + // credentials: 'include', } // console.log(headers) // headers['X-CSRFToken'] = csrftoken diff --git a/package-lock.json b/package-lock.json index a42dca34..da0dfcae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2007,9 +2007,9 @@ } }, "connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true }, "connected-react-router": { @@ -3427,6 +3427,11 @@ "strip-eof": "^1.0.0" } }, + "exifreader": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/exifreader/-/exifreader-2.5.0.tgz", + "integrity": "sha512-jxS+cSjalvtF4Ga1D0G1YbVwPBOUYIGMuVbUNfor1+5CSc4h2FBvFC59HSkpPTqiHnA9zF0W5BW8eV3CjmklcQ==" + }, "exit-hook": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", @@ -3493,7 +3498,7 @@ "dependencies": { "array-flatten": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true } @@ -3628,7 +3633,7 @@ }, "finalhandler": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "dev": true, "requires": { @@ -3681,9 +3686,9 @@ } }, "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz", + "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==", "dev": true, "requires": { "debug": "=3.1.0" @@ -4400,7 +4405,7 @@ }, "globby": { "version": "6.1.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -4413,7 +4418,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -4425,9 +4430,9 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "handle-thing": { - "version": "1.2.5", - "resolved": "http://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", - "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", "dev": true }, "has": { @@ -4715,7 +4720,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { @@ -4744,7 +4749,7 @@ }, "http-proxy-middleware": { "version": "0.18.0", - "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", "dev": true, "requires": { @@ -4830,7 +4835,7 @@ }, "is-accessor-descriptor": { "version": "0.1.6", - "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { @@ -4850,7 +4855,7 @@ }, "is-data-descriptor": { "version": "0.1.4", - "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { @@ -5828,7 +5833,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, @@ -6712,7 +6717,7 @@ "dependencies": { "async": { "version": "1.5.2", - "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true } @@ -8338,32 +8343,75 @@ "dev": true }, "spdy": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", - "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.0.tgz", + "integrity": "sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==", "dev": true, "requires": { - "debug": "^2.6.8", - "handle-thing": "^1.2.5", + "debug": "^4.1.0", + "handle-thing": "^2.0.0", "http-deceiver": "^1.2.7", - "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", - "spdy-transport": "^2.0.18" + "spdy-transport": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } } }, "spdy-transport": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.1.tgz", - "integrity": "sha512-q7D8c148escoB3Z7ySCASadkegMmUZW8Wb/Q1u0/XBgDKMO880rLQDj8Twiew/tYi7ghemKUi/whSYOwE17f5Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, "requires": { - "debug": "^2.6.8", - "detect-node": "^2.0.3", + "debug": "^4.1.0", + "detect-node": "^2.0.4", "hpack.js": "^2.1.6", - "obuf": "^1.1.1", - "readable-stream": "^2.2.9", - "safe-buffer": "^5.0.1", - "wbuf": "^1.7.2" + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "readable-stream": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", + "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "split-string": { @@ -9902,9 +9950,9 @@ } }, "webpack-dev-server": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz", - "integrity": "sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz", + "integrity": "sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -9926,12 +9974,14 @@ "portfinder": "^1.0.9", "schema-utils": "^1.0.0", "selfsigned": "^1.9.1", + "semver": "^5.6.0", "serve-index": "^1.7.2", "sockjs": "0.3.19", "sockjs-client": "1.3.0", - "spdy": "^3.4.1", + "spdy": "^4.0.0", "strip-ansi": "^3.0.0", "supports-color": "^5.1.0", + "url": "^0.11.0", "webpack-dev-middleware": "3.4.0", "webpack-log": "^2.0.0", "yargs": "12.0.2" @@ -10083,13 +10133,13 @@ } }, "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, "requires": { "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", + "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", @@ -10141,7 +10191,7 @@ }, "is-accessor-descriptor": { "version": "0.1.6", - "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { @@ -10161,7 +10211,7 @@ }, "is-data-descriptor": { "version": "0.1.4", - "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { @@ -10257,6 +10307,15 @@ } } }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -10402,12 +10461,12 @@ } }, "os-locale": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz", - "integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "dev": true, "requires": { - "execa": "^0.10.0", + "execa": "^1.0.0", "lcid": "^2.0.0", "mem": "^4.0.0" } @@ -10421,6 +10480,16 @@ "has-flag": "^3.0.0" } }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, "yargs": { "version": "12.0.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", diff --git a/package.json b/package.json index feef9b94..1848cc7c 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "data-uri-to-buffer": "^2.0.0", "date-fns": "^1.29.0", "dotenv": "^6.0.0", + "exifreader": "^2.5.0", "fetch-jsonp": "^1.1.3", "file-saver": "^2.0.0-rc.3", "history": "^4.7.2", @@ -72,6 +73,6 @@ "uglifyjs-webpack-plugin": "^1.3.0", "webpack": "3.x.x", "webpack-cli": "^3.1.0", - "webpack-dev-server": "^3.1.10" + "webpack-dev-server": "^3.1.14" } } -- cgit v1.2.3-70-g09d2 From 47b6ae0f8ad2f49692222bb0c800e7ba1eb4b94b Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 00:54:13 +0100 Subject: face info page --- client/faceAnalysis/faceAnalysis.actions.js | 20 +-- client/faceAnalysis/faceAnalysis.query.js | 4 +- client/faceAnalysis/faceAnalysis.reducer.js | 7 + client/index.js | 2 + client/types.js | 2 +- megapixels/app/server/api.py | 1 - megapixels/app/server/api_task.py | 40 +++--- megapixels/app/server/tasks/__init__.py | 19 ++- megapixels/app/server/tasks/blur.py | 23 +--- megapixels/app/server/tasks/fullmonte.py | 199 ++++++++++++++++++++++++++++ megapixels/app/utils/im_utils.py | 12 ++ site/public/info/index.html | 53 ++++++++ 12 files changed, 322 insertions(+), 60 deletions(-) create mode 100644 megapixels/app/server/tasks/fullmonte.py create mode 100644 site/public/info/index.html diff --git a/client/faceAnalysis/faceAnalysis.actions.js b/client/faceAnalysis/faceAnalysis.actions.js index 90d7156f..6a318b5d 100644 --- a/client/faceAnalysis/faceAnalysis.actions.js +++ b/client/faceAnalysis/faceAnalysis.actions.js @@ -8,7 +8,7 @@ import { get, post } from '../util' // urls const url = { - upload: (dataset) => process.env.API_HOST + '/api/dataset/' + dataset + '/face', + upload: () => process.env.API_HOST + '/task/upload/sleep', } export const publicUrl = { } @@ -26,6 +26,11 @@ const loaded = (tag, data, offset = 0) => ({ data, offset }) +const polled = (data, offset = 0) => ({ + type: types.faceAnalysis.poll, + data, + offset +}) const error = (tag, err) => ({ type: types.faceAnalysis.error, tag, @@ -42,7 +47,7 @@ export const updateOptions = opt => dispatch => { export const upload = (payload, file) => dispatch => { // const { options } = store.getState().faceAnalysis - const tag = 'result' + const tag = 'task' const fd = new FormData() fd.append('query_img', file) // fd.append('limit', options.perPage) @@ -62,14 +67,13 @@ const POLL_DELAY = 500 let pollTimeout = null export const poll = (payload, taskURL) => dispatch => { - const tag = 'poll' clearTimeout(pollTimeout) - dispatch(loading(tag)) get(taskURL) .then(data => { - dispatch(loaded(tag, data)) - // check if complete - pollTimeout = setTimeout(() => poll(payload, taskURL), POLL_DELAY) + dispatch(polled(data)) + if (!data.complete) { + pollTimeout = setTimeout(() => poll(payload, taskURL), POLL_DELAY) + } }) - .catch(err => dispatch(error(tag, err))) + .catch(err => dispatch(error('result', err))) } diff --git a/client/faceAnalysis/faceAnalysis.query.js b/client/faceAnalysis/faceAnalysis.query.js index 86dbe1ae..6b92b70d 100644 --- a/client/faceAnalysis/faceAnalysis.query.js +++ b/client/faceAnalysis/faceAnalysis.query.js @@ -57,8 +57,8 @@ class FaceAnalysisQuery extends Component { {'Put yourself under the microscope of various facial recognition algorithms. See what can be determined from a photo.'}

    -
  1. Upload a photo of yourself
  2. -
  3. {'Your search data is never stored and immediately cleared '} +
  4. Upload a photo of yourself and be judged by the algorithm
  5. +
  6. {'Your search data is only stored for the duration of this analysis and is immediately cleared '} {'once you leave this page.'}

diff --git a/client/faceAnalysis/faceAnalysis.reducer.js b/client/faceAnalysis/faceAnalysis.reducer.js index d8e914ab..54a6d5eb 100644 --- a/client/faceAnalysis/faceAnalysis.reducer.js +++ b/client/faceAnalysis/faceAnalysis.reducer.js @@ -2,6 +2,7 @@ import * as types from '../types' const initialState = () => ({ query: {}, + task: {}, result: {}, loading: false, }) @@ -20,6 +21,12 @@ export default function faceAnalysisReducer(state = initialState(), action) { [action.tag]: action.data, } + case types.faceAnalysis.poll: + return { + ...state, + result: action.data, + } + case types.faceAnalysis.error: return { ...state, diff --git a/client/index.js b/client/index.js index 40be2841..96f2c8c8 100644 --- a/client/index.js +++ b/client/index.js @@ -20,6 +20,8 @@ function appendReactApplet(el, payload) { } function fetchDataset(payload) { + if (payload.command === 'face_analysis') return new Promise(resolve => resolve()) + if (payload.dataset === 'info') return new Promise(resolve => resolve()) const url = "https://megapixels.nyc3.digitaloceanspaces.com/v1/citations/" + payload.dataset + ".json" return fetch(url, { mode: 'cors' }).then(r => r.json()) } diff --git a/client/types.js b/client/types.js index 2d35ec36..fd9aa3e0 100644 --- a/client/types.js +++ b/client/types.js @@ -7,7 +7,7 @@ export const tagAsType = (type, names) => ( ) export const faceAnalysis = tagAsType('faceAnalysis', [ - 'loading', 'loaded', 'error', 'update_options', + 'loading', 'loaded', 'poll', 'error', 'update_options', ]) export const faceSearch = tagAsType('faceSearch', [ diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 48279040..663f52cc 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -30,7 +30,6 @@ def index(): """List the datasets and their fields""" return jsonify({ 'datasets': list_datasets() }) - @api.route('/dataset/') def show(dataset_name): """Show the data that a dataset will return""" diff --git a/megapixels/app/server/api_task.py b/megapixels/app/server/api_task.py index 36990997..23e11454 100644 --- a/megapixels/app/server/api_task.py +++ b/megapixels/app/server/api_task.py @@ -6,30 +6,21 @@ import dlib import simplejson as json import numpy as np from flask import Blueprint, request, jsonify -from PIL import Image # todo: try to remove PIL dependency - -# from app.models.sql_factory import load_sql_datasets, list_datasets, get_dataset, get_table -# from app.utils.im_utils import pil2np +from PIL import Image, ImageOps # todo: try to remove PIL dependency from celery.result import AsyncResult from app.server.tasks import celery from app.server.tasks import task_lookup, list_active_tasks +# from app.models.sql_factory import load_sql_datasets, list_datasets, get_dataset, get_table -api_task = Blueprint('api_task', __name__) +api_task = Blueprint('task', __name__) @api_task.route('/') def index(): """List active tasks""" return jsonify(list_active_tasks) -# from flask import render_template, redirect, url_for, send_from_directory -# from flask import request, make_response, jsonify -# from . import main, utils -# from PIL import Image, ImageOps -# import cv2 as cv -# import imutils - -@api_task.route('//') +@api_task.route('/status//') def task_status(task_name, task_id): """Return celery image processing status""" if task_name in task_lookup: @@ -69,6 +60,9 @@ def task_status(task_name, task_id): @api_task.route('/upload/sleep', methods=['GET', 'POST']) def sleep_test(): + """ + Test the Celery system using a task that sleeps. + """ async_task = task_lookup['sleep']['task'].apply_async(args=['sleep_test']) task_url = '/task/{}/{}'.format('sleep', async_task.id) return jsonify({ @@ -76,10 +70,12 @@ def sleep_test(): 'task_url': task_url, }) -@api_task.route('/upload', methods=['POST']) -def upload(): - style = request.form['style'] - print('style',style) +@api_task.route('/upload/:style', methods=['POST']) +def upload(style): + """ + Process a images in a particular style + """ + print('style: {}'.format(style)) if style in task_lookup: task = task_lookup[style]['task'] print('task',task) @@ -103,19 +99,19 @@ def upload(): # convert PNG to JPG print('[+] Resizing image') - # LOL MaskRCNN needs to be run outside of the Celery Task im = Image.open(file.stream).convert('RGB') - im = ImageOps.fit(im,(512,512)) + im = ImageOps.fit(im, (256, 256)) # # Save image to disk # print('[+] Save image to {}'.format(fpath)) # im.save(fpath, 'JPEG', quality=100) # im_pil_256 = im.resize((256,256)) - print('[+] ensure_np...') - im_np = imx.ensure_np(im_pil_256) + # print('[+] ensure_np...') + # im_np = imx.ensure_np(im_pil_256) celery_result = { + im: im, } print('[+] Start celery') @@ -124,6 +120,6 @@ def upload(): return jsonify({ 'result': True, - 'task_url': task_url, + 'taskURL': task_url, 'uuid': uuid_name }) diff --git a/megapixels/app/server/tasks/__init__.py b/megapixels/app/server/tasks/__init__.py index bac7309f..fd6e398a 100644 --- a/megapixels/app/server/tasks/__init__.py +++ b/megapixels/app/server/tasks/__init__.py @@ -5,7 +5,7 @@ from celery import Celery celery = Celery(__name__, backend=cfg.CELERY_RESULT_BACKEND, broker=cfg.CELERY_BROKER_URL) from app.server.tasks.sleep import sleep_task -# from app.server.tasks.blur import blur_task +from app.server.tasks.blur import blur_task def list_active_tasks(): dropdown = {} @@ -32,16 +32,15 @@ task_lookup = { 'active': True, 'default': True, }, - # 'blur': { - # 'title': 'Blur', - # 'task': blur_task, - # 'active': False, - # }, - # 'task_dull': { - # 'title': 'DullDream V2', - # 'task': task_dull, + 'blur': { + 'title': 'Blur', + 'task': blur_task, + 'active': False, + }, + # 'fullmonte': { + # 'title': 'TIA facial processing pipeline', + # 'task': fullmonte, # 'active': True, # 'default': True, # } } - diff --git a/megapixels/app/server/tasks/blur.py b/megapixels/app/server/tasks/blur.py index ede75e6a..3b7e20be 100644 --- a/megapixels/app/server/tasks/blur.py +++ b/megapixels/app/server/tasks/blur.py @@ -3,14 +3,14 @@ import sys import time import datetime import json -from PIL import Image, ImageFilter +from PIL import Image import cv2 as cv import numpy as np -from . import main, utils -from .. import basemodels +from app.utils.im_utils import ensure_np, ensure_pil from flask import current_app as app -from .paths import get_paths -celery = basemodels.celery + +from app.server.tasks import celery + from celery.utils.log import get_task_logger celery_logger = get_task_logger(__name__) import imutils @@ -37,9 +37,9 @@ def blur_task(self, uuid_name, extra): 'uuid': uuid_name }) - im_np = utils.ensure_np(im) + im_np = ensure_np(im) im_blur = cv.blur(im_np, (5,5), 1.0) - im_blur_pil = utils.ensure_pil(im_blur) + im_blur_pil = ensure_pil(im_blur) fn = uuid_name + '_blur.jpg' fpath = os.path.join(render_dir, fn) @@ -52,15 +52,6 @@ def blur_task(self, uuid_name, extra): time.sleep(3) - self.update_state( - state = 'PROCESSING', - meta = { - 'percent': 0.50, - 'message': 'Sleeping for some reason', - 'uuid': uuid_name - }) - time.sleep(2) - self.update_state( state = 'PROCESSING', meta = { diff --git a/megapixels/app/server/tasks/fullmonte.py b/megapixels/app/server/tasks/fullmonte.py new file mode 100644 index 00000000..17ca9403 --- /dev/null +++ b/megapixels/app/server/tasks/fullmonte.py @@ -0,0 +1,199 @@ + +import sys +import os +from os.path import join +from pathlib import Path +import time + +import numpy as np +import cv2 as cv +import dlib +from PIL import Image +import matplotlib.pyplot as plt + +from app.utils import logger_utils, file_utils, im_utils, display_utils, draw_utils +from app.utils import plot_utils +from app.processors import face_detector, face_landmarks +from app.models.data_store import DataStore + +@celery.task(bind=True) +def fullmonte_task(self, uuid_name): + return + + # TOOD add selective testing + opt_run_pose = True + opt_run_2d_68 = True + opt_run_3d_68 = True + opt_run_3d_68 = True + + # ------------------------------------------------- + # init here + + + log = logger_utils.Logger.getLogger() + + # load image + im = cv.imread(opt_fp_in) + im_resized = im_utils.resize(im, width=opt_size[0], height=opt_size[1]) + + + # ---------------------------------------------------------------------------- + # detect face + + face_detector = face_detector.DetectorDLIBCNN(gpu=opt_gpu) # -1 for CPU + log.info('detecting face...') + st = time.time() + bboxes = face_detector.detect(im_resized, largest=True) + bbox = bboxes[0] + dim = im_resized.shape[:2][::-1] + bbox_dim = bbox.to_dim(dim) + if not bbox: + log.error('no face detected') + return + else: + log.info(f'Detected face in {(time.time() - st):.2f}s') + log.info('') + + + # ---------------------------------------------------------------------------- + # detect 3D landmarks + + log.info('loading 3D landmark generator files...') + landmark_detector_3d_68 = face_landmarks.FaceAlignment3D_68(gpu=opt_gpu) # -1 for CPU + log.info('generating 3D landmarks...') + st = time.time() + points_3d_68 = landmark_detector_3d_68.landmarks(im_resized, bbox_dim.to_xyxy()) + log.info(f'generated 3D landmarks in {(time.time() - st):.2f}s') + log.info('') + + + # ---------------------------------------------------------------------------- + # generate 3D GIF animation + + log.info('generating 3D animation...') + if not opt_fp_out: + fpp_im = Path(opt_fp_in) + fp_out = join(fpp_im.parent, f'{fpp_im.stem}_anim.gif') + else: + fp_out = opt_fp_out + st = time.time() + plot_utils.generate_3d_landmark_anim(np.array(points_3d_68), fp_out, + size=opt_gif_size, num_frames=opt_gif_frames) + log.info(f'Generated animation in {(time.time() - st):.2f}s') + log.info(f'Saved to: {fp_out}') + log.info('') + + + # ---------------------------------------------------------------------------- + # generate face vectors, only to test if feature extraction works + + log.info('initialize face recognition model...') + from app.processors import face_recognition + face_rec = face_recognition.RecognitionDLIB() + st = time.time() + log.info('generating face vector...') + vec = face_rec.vec(im_resized, bbox_dim) + log.info(f'generated face vector in {(time.time() - st):.2f}s') + log.info('') + + + # ---------------------------------------------------------------------------- + # generate 68 point landmarks using dlib + + log.info('initializing face landmarks 68 dlib...') + from app.processors import face_landmarks + landmark_detector_2d_68 = face_landmarks.Dlib2D_68() + log.info('generating 2D 68PT landmarks...') + st = time.time() + points_2d_68 = landmark_detector_2d_68.landmarks(im_resized, bbox_dim) + log.info(f'generated 2D 68PT face landmarks in {(time.time() - st):.2f}s') + log.info('') + + + # ---------------------------------------------------------------------------- + # generate pose from 68 point 2D landmarks + + if opt_run_pose: + log.info('initialize pose...') + from app.processors import face_pose + pose_detector = face_pose.FacePoseDLIB() + log.info('generating pose...') + st = time.time() + pose_data = pose_detector.pose(points_2d_68, dim) + log.info(f'generated pose {(time.time() - st):.2f}s') + log.info('') + + + # ---------------------------------------------------------------------------- + # generate pose from 68 point 2D landmarks + + # done + self.log.debug('Add age real') + self.log.debug('Add age apparent') + self.log.debug('Add gender') + + + # 3DDFA + self.log.debug('Add depth') + self.log.debug('Add pncc') + + # TODO + self.log.debug('Add 3D face model') + self.log.debug('Add face texture flat') + self.log.debug('Add ethnicity') + + # display + if opt_display: + + # draw bbox + + # draw 3d landmarks + im_landmarks_3d_68 = im_resized.copy() + draw_utils.draw_landmarks3D(im_landmarks_3d_68, points_3d_68) + draw_utils.draw_bbox(im_landmarks_3d_68, bbox_dim) + + # draw 2d landmarks + im_landmarks_2d_68 = im_resized.copy() + draw_utils.draw_landmarks2D(im_landmarks_2d_68, points_2d_68) + draw_utils.draw_bbox(im_landmarks_2d_68, bbox_dim) + + # draw pose + if opt_run_pose: + im_pose = im_resized.copy() + draw_utils.draw_pose(im_pose, pose_data['point_nose'], pose_data['points']) + draw_utils.draw_degrees(im_pose, pose_data) + + # draw animated GIF + im = Image.open(fp_out) + im_frames = [] + duration = im.info['duration'] + try: + while True: + im.seek(len(im_frames)) + mypalette = im.getpalette() + im.putpalette(mypalette) + im_jpg = Image.new("RGB", im.size) + im_jpg.paste(im) + im_np = im_utils.pil2np(im_jpg.copy()) + im_frames.append(im_np) + except EOFError: + pass # end of GIF sequence + + n_frames = len(im_frames) + frame_number = 0 + + while True: + # show all images here + cv.imshow('Original', im_resized) + cv.imshow('2D 68PT Landmarks', im_landmarks_2d_68) + cv.imshow('3D 68PT Landmarks', im_landmarks_3d_68) + cv.imshow('Pose', im_pose) + cv.imshow('3D 68pt GIF', im_frames[frame_number]) + frame_number = (frame_number + 1) % n_frames + k = cv.waitKey(duration) & 0xFF + if k == 27 or k == ord('q'): # ESC + cv.destroyAllWindows() + sys.exit() + elif k != 255: + # any key to continue + break \ No newline at end of file diff --git a/megapixels/app/utils/im_utils.py b/megapixels/app/utils/im_utils.py index e882c67f..d36c1c32 100644 --- a/megapixels/app/utils/im_utils.py +++ b/megapixels/app/utils/im_utils.py @@ -19,7 +19,19 @@ from torch.autograd import Variable from sklearn.metrics.pairwise import cosine_similarity import datetime +def ensure_pil(im): + """Ensure image is Pillow format""" + try: + im.verify() + return im + except: + return Image.fromarray(im.astype('uint8'), 'RGB') +def ensure_np(im): + """Ensure image is numpy array""" + if type(im) == np.ndarray: + return im + return np.asarray(im, np.uint8) def num_channels(im): '''Returns number of channels in numpy.ndarray image''' diff --git a/site/public/info/index.html b/site/public/info/index.html new file mode 100644 index 00000000..0d7b2d2e --- /dev/null +++ b/site/public/info/index.html @@ -0,0 +1,53 @@ + + + + MegaPixels + + + + + + + + + + + + +

+ + +
MegaPixels
+ The Darkside of Datasets +
+ +
+
+ +

What do facial recognition algorithms see?

+

Results are only stored for the duration of the analysis and are deleted when you leave this page.

+
+ +
+ + + + + \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 6710b9f7f223acd01ac82171d9f9f4eb577f5885 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 18:08:49 +0100 Subject: serializing image failed, writing to tmp file instead --- client/common/upload.helpers.js | 6 ++-- client/common/uploadImage.component.js | 3 +- client/faceAnalysis/faceAnalysis.actions.js | 39 +++++++++++---------- client/faceAnalysis/faceAnalysis.query.js | 10 ++---- client/faceAnalysis/faceAnalysis.reducer.js | 1 + client/faceAnalysis/faceAnalysis.result.js | 54 +++-------------------------- megapixels/app/server/api_task.py | 48 ++++++++++--------------- megapixels/app/server/tasks/blur.py | 27 ++++++--------- package-lock.json | 29 +++++++++------- package.json | 1 + 10 files changed, 81 insertions(+), 137 deletions(-) diff --git a/client/common/upload.helpers.js b/client/common/upload.helpers.js index 5a041fd4..eb42a993 100644 --- a/client/common/upload.helpers.js +++ b/client/common/upload.helpers.js @@ -15,15 +15,15 @@ function base64ToUint8Array(string, start, finish) { } function getOrientation(uri) { - const exif = new ExifReader() // Split off the base64 data const base64String = uri.split(',')[1] // Read off first 128KB, which is all we need to // get the EXIF data const arr = base64ToUint8Array(base64String, 0, 2 ** 17) try { - exif.load(arr.buffer) - return exif.getTagValue('Orientation') + const tags = ExifReader.load(arr.buffer) + // console.log(tags) + return tags.Orientation } catch (err) { return 1 } diff --git a/client/common/uploadImage.component.js b/client/common/uploadImage.component.js index eb8cc60f..bc88828e 100644 --- a/client/common/uploadImage.component.js +++ b/client/common/uploadImage.component.js @@ -20,7 +20,7 @@ export default class UploadImageComponent extends Component { img.onload = null this.resizeAndUpload(img) } - img.src = fileReaderEvent.result + img.src = fileReaderEvent.target.result } fr.readAsDataURL(files[0]) } @@ -28,6 +28,7 @@ export default class UploadImageComponent extends Component { resizeAndUpload(img) { const canvas = renderThumbnail(img) canvas.toBlob(blob => { + // console.log(blob) this.props.onUpload(blob) }, 'image/jpeg', 80) } diff --git a/client/faceAnalysis/faceAnalysis.actions.js b/client/faceAnalysis/faceAnalysis.actions.js index 6a318b5d..860d3292 100644 --- a/client/faceAnalysis/faceAnalysis.actions.js +++ b/client/faceAnalysis/faceAnalysis.actions.js @@ -8,7 +8,7 @@ import { get, post } from '../util' // urls const url = { - upload: () => process.env.API_HOST + '/task/upload/sleep', + upload: () => process.env.API_HOST + '/task/upload/blur', } export const publicUrl = { } @@ -45,22 +45,6 @@ export const updateOptions = opt => dispatch => { // API functions -export const upload = (payload, file) => dispatch => { - // const { options } = store.getState().faceAnalysis - const tag = 'task' - const fd = new FormData() - fd.append('query_img', file) - // fd.append('limit', options.perPage) - // if (!query) { - dispatch(loading(tag)) - // } - post(url.upload(payload.dataset), fd) - .then(data => { - dispatch(loaded(tag, data)) - }) - .catch(err => dispatch(error(tag, err))) -} - // task polling const POLL_DELAY = 500 @@ -68,12 +52,31 @@ let pollTimeout = null export const poll = (payload, taskURL) => dispatch => { clearTimeout(pollTimeout) + console.log('polling...') get(taskURL) .then(data => { + console.log('poll', data) dispatch(polled(data)) - if (!data.complete) { + if (data.state !== 'error' && data.state !== 'complete') { pollTimeout = setTimeout(() => poll(payload, taskURL), POLL_DELAY) } }) .catch(err => dispatch(error('result', err))) } + +export const upload = (payload, file) => dispatch => { + const tag = 'task' + const fd = new FormData() + fd.append('query_img', file) + dispatch(loading(tag)) + post(url.upload(), fd) + .then(data => { + console.log('loaded!', tag, data) + dispatch(loaded(tag, data)) + const { result, taskURL } = data + if (result && taskURL) { + poll(payload, taskURL)(dispatch) + } + }) + .catch(err => dispatch(error(tag, err))) +} diff --git a/client/faceAnalysis/faceAnalysis.query.js b/client/faceAnalysis/faceAnalysis.query.js index 6b92b70d..a79e3e78 100644 --- a/client/faceAnalysis/faceAnalysis.query.js +++ b/client/faceAnalysis/faceAnalysis.query.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' -import { Loader } from '../common' +import { Loader, UploadImage } from '../common' import * as actions from './faceAnalysis.actions' // function parse_bbox(s) { @@ -37,13 +37,7 @@ class FaceAnalysisQuery extends Component {
{image ? null : } - +
{result.loading && (
diff --git a/client/faceAnalysis/faceAnalysis.reducer.js b/client/faceAnalysis/faceAnalysis.reducer.js index 54a6d5eb..de6e5b0a 100644 --- a/client/faceAnalysis/faceAnalysis.reducer.js +++ b/client/faceAnalysis/faceAnalysis.reducer.js @@ -28,6 +28,7 @@ export default function faceAnalysisReducer(state = initialState(), action) { } case types.faceAnalysis.error: + console.log('error', action) return { ...state, [action.tag]: { error: action.err }, diff --git a/client/faceAnalysis/faceAnalysis.result.js b/client/faceAnalysis/faceAnalysis.result.js index b825a0cb..f9531eba 100644 --- a/client/faceAnalysis/faceAnalysis.result.js +++ b/client/faceAnalysis/faceAnalysis.result.js @@ -26,12 +26,6 @@ const errors = { {"Sorry, an error occured."}
), - bad_dataset: ( -
-

{""}

- {""} -
- ), not_an_image: (

{"Not an image"}

@@ -42,19 +36,19 @@ const errors = { class FaceAnalysisResult extends Component { render() { - const { dataset } = this.props.payload - const { query, distances, results, loading, error } = this.props.result + const { query, task, result, loading, error } = this.props.result console.log(this.props.result) if (loading) { return (

-

Searching...

+

Uploading...

) } + console.log(task, result) if (error) { // console.log(error) let errorMessage = errors[error] || errors.error @@ -62,53 +56,13 @@ class FaceAnalysisResult extends Component {
{errorMessage}
) } - if (!results) { - return
- } - if (!results.length) { - return ( -
{errors.nomatch}
- ) - } - const els = results.map((result, i) => { - const distance = distances[i] - const { uuid } = result.file_record - const { x, y, w, h } = result.face_roi - const { fullname, gender, description, images } = result.identity - const bbox = { - left: (100 * x) + '%', - top: (100 * y) + '%', - width: (100 * w) + '%', - height: (100 * h) + '%', - } - // console.log(bbox) - return ( -
-
- -
-
- {fullname} {'('}{gender}{')'}
- {description}
- {courtesyS(images, 'image')}{' in dataset'}
- {Math.round((1 - distance) * 100)}{'% match'} -
- ) - }) + if (!task && !result) return return (
-

Did we find you?

- {'These faces matched images in the '} - {dataset} - {' dataset with over 70% probability.'} -
Query took {query.timing.toFixed(2)} seconds
-
- {els} -
) } diff --git a/megapixels/app/server/api_task.py b/megapixels/app/server/api_task.py index 23e11454..fb24c154 100644 --- a/megapixels/app/server/api_task.py +++ b/megapixels/app/server/api_task.py @@ -3,6 +3,7 @@ import re import uuid import time import dlib +import tempfile import simplejson as json import numpy as np from flask import Blueprint, request, jsonify @@ -64,62 +65,51 @@ def sleep_test(): Test the Celery system using a task that sleeps. """ async_task = task_lookup['sleep']['task'].apply_async(args=['sleep_test']) - task_url = '/task/{}/{}'.format('sleep', async_task.id) + task_url = '/task/status/{}/{}'.format('sleep', async_task.id) return jsonify({ 'result': True, 'task_url': task_url, }) -@api_task.route('/upload/:style', methods=['POST']) -def upload(style): +@api_task.route('/upload/blur', methods=['POST']) +def upload(): """ Process a images in a particular style """ + style = 'blur' print('style: {}'.format(style)) if style in task_lookup: task = task_lookup[style]['task'] - print('task',task) + print('task', task) else: return jsonify({ 'result': False, 'error': 'Unknown task', }) - file = request.files['user_image'] - ext = request.form['ext'] - if ext is None: - ext = request.files['ext'] + print('get file...') + file = request.files['query_img'] - uuid_name = str(uuid.uuid4()) + uuid_str = str(uuid.uuid4()) - app.logger.info('[+] style: {}'.format(style)) - app.logger.info('[+] ext: {}'.format(ext)) - app.logger.info('[+] uuid_name: {}'.format(uuid_name)) - - # convert PNG to JPG - print('[+] Resizing image') + print('[+] style: {}'.format(style)) + print('[+] uuid_name: {}'.format(uuid_str)) im = Image.open(file.stream).convert('RGB') - im = ImageOps.fit(im, (256, 256)) - - # # Save image to disk - # print('[+] Save image to {}'.format(fpath)) - # im.save(fpath, 'JPEG', quality=100) - # im_pil_256 = im.resize((256,256)) + im = ImageOps.fit(im, (256, 256,)) - # print('[+] ensure_np...') - # im_np = imx.ensure_np(im_pil_256) + tmpfile = tempfile.NamedTemporaryFile(delete=False) - celery_result = { - im: im, - } + # Save image to disk + print('[+] Save image to temporary file') + im.save(tmpfile, 'JPEG', quality=80) print('[+] Start celery') - async_task = task.apply_async(args=[uuid_name, celery_result]) - task_url = '/task/{}/{}'.format(style, async_task.id) + async_task = task.apply_async(args=[uuid_str, tmpfile.name]) + task_url = '/task/status/{}/{}'.format(style, async_task.id) return jsonify({ 'result': True, 'taskURL': task_url, - 'uuid': uuid_name + 'uuid': uuid_str }) diff --git a/megapixels/app/server/tasks/blur.py b/megapixels/app/server/tasks/blur.py index 3b7e20be..d1f67f54 100644 --- a/megapixels/app/server/tasks/blur.py +++ b/megapixels/app/server/tasks/blur.py @@ -16,18 +16,13 @@ celery_logger = get_task_logger(__name__) import imutils @celery.task(bind=True) -def blur_task(self, uuid_name, extra): +def blur_task(self, uuid_name, fn): """Process image and update during""" celery_logger.debug('process_image_task, uuid: {}'.format(uuid_name)) files = [] - im = Image.open(os.path.join(upload_dir, uuid_name + '.jpg')).convert('RGB') - im = im.resize((256,256)) - files.append({ - 'title': 'Original image', - 'fn': upload_uri + uuid_name + '.jpg' - }) + im = Image.open(fn).convert('RGB') self.update_state( state = 'PROCESSING', @@ -42,13 +37,13 @@ def blur_task(self, uuid_name, extra): im_blur_pil = ensure_pil(im_blur) fn = uuid_name + '_blur.jpg' - fpath = os.path.join(render_dir, fn) - im_blur_pil.save(fpath, 'JPEG', quality=95) + # fpath = os.path.join(render_dir, fn) + # im_blur_pil.save(fpath, 'JPEG', quality=95) - files.append({ - 'title': 'Blurred image', - 'fn': render_uri + uuid_name + '_blur.jpg' - }) + # files.append({ + # 'title': 'Blurred image', + # 'fn': render_uri + uuid_name + '_blur.jpg' + # }) time.sleep(3) @@ -67,9 +62,9 @@ def blur_task(self, uuid_name, extra): 'files': files } - json_path = os.path.join(json_dir, uuid_name + '.json') - with open(json_path, 'w') as json_file: - json.dump(data, json_file) + # json_path = os.path.join(json_dir, uuid_name + '.json') + # with open(json_path, 'w') as json_file: + # json.dump(data, json_file) celery_logger.debug('ok') diff --git a/package-lock.json b/package-lock.json index da0dfcae..60a74ece 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3427,6 +3427,11 @@ "strip-eof": "^1.0.0" } }, + "exif-reader": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-1.0.2.tgz", + "integrity": "sha1-AkCLl7YQKOpPReW4k6g2+aoorE8=" + }, "exifreader": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/exifreader/-/exifreader-2.5.0.tgz", @@ -3498,7 +3503,7 @@ "dependencies": { "array-flatten": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true } @@ -3633,7 +3638,7 @@ }, "finalhandler": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "dev": true, "requires": { @@ -4405,7 +4410,7 @@ }, "globby": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -4418,7 +4423,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -4720,7 +4725,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { @@ -4749,7 +4754,7 @@ }, "http-proxy-middleware": { "version": "0.18.0", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", "dev": true, "requires": { @@ -4835,7 +4840,7 @@ }, "is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { @@ -4855,7 +4860,7 @@ }, "is-data-descriptor": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { @@ -5833,7 +5838,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, @@ -6717,7 +6722,7 @@ "dependencies": { "async": { "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true } @@ -10191,7 +10196,7 @@ }, "is-accessor-descriptor": { "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { @@ -10211,7 +10216,7 @@ }, "is-data-descriptor": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { diff --git a/package.json b/package.json index 1848cc7c..fcabb7e1 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "data-uri-to-buffer": "^2.0.0", "date-fns": "^1.29.0", "dotenv": "^6.0.0", + "exif-reader": "^1.0.2", "exifreader": "^2.5.0", "fetch-jsonp": "^1.1.3", "file-saver": "^2.0.0-rc.3", -- cgit v1.2.3-70-g09d2 From b4ed297a6dc73ec5f5cf2772ca1b754ea3f98cae Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 21:06:51 +0100 Subject: basic blurring applet --- .gitignore | 1 + client/common/upload.helpers.js | 27 +++++------ client/faceAnalysis/faceAnalysis.actions.js | 21 ++++++--- client/faceAnalysis/faceAnalysis.container.js | 2 +- client/faceAnalysis/faceAnalysis.query.js | 12 ++++- client/faceAnalysis/faceAnalysis.reducer.js | 8 ++++ client/faceAnalysis/faceAnalysis.result.js | 26 ++++++++--- megapixels/app/server/api.py | 1 - megapixels/app/server/api_task.py | 10 ++-- megapixels/app/server/tasks/blur.py | 67 ++++++++++++++------------- megapixels/app/server/tasks/fullmonte.py | 6 +-- megapixels/app/server/tasks/sleep.py | 2 +- megapixels/app/settings/app_cfg.py | 1 + site/assets/css/applets.css | 17 ++++++- 14 files changed, 132 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index 30c69fbe..b800c5b8 100644 --- a/.gitignore +++ b/.gitignore @@ -158,4 +158,5 @@ scraper/reports/papers/ .creds site/assets/js/dist/ +site/public/user_content diff --git a/client/common/upload.helpers.js b/client/common/upload.helpers.js index eb42a993..4b38fb09 100644 --- a/client/common/upload.helpers.js +++ b/client/common/upload.helpers.js @@ -1,6 +1,6 @@ import ExifReader from 'exifreader' -export const MAX_SIDE = 300 +export const MAX_SIDE = 256 function base64ToUint8Array(string, start, finish) { start = start || 0 @@ -110,16 +110,17 @@ export function renderToCanvas(img, options) { options = options || {} // Canvas max size for any side - const maxSize = MAX_SIDE + const maxSide = MAX_SIDE const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') const initialScale = options.scale || 1 // Scale to needed to constrain canvas to max size - let scale = getScale(img.width * initialScale, img.height * initialScale, maxSize, maxSize, true) + let scale = getScale(img.naturalWidth * initialScale, img.naturalHeight * initialScale, maxSide, maxSide, true) + console.log(scale) // Still need to apply the user defined scale scale *= initialScale - canvas.width = Math.round(img.width * scale) - canvas.height = Math.round(img.height * scale) + canvas.width = Math.round(img.naturalWidth * scale) + canvas.height = Math.round(img.naturalHeight * scale) const { correctOrientation } = options const jpeg = !!img.src.match(/data:image\/jpeg|\.jpeg$|\.jpg$/i) const hasDataURI = !!img.src.match(/^data:/) @@ -144,12 +145,12 @@ export function renderToCanvas(img, options) { export function renderThumbnail(img) { const resized = renderToCanvas(img, { correctOrientation: true }) - const canvas = document.createElement('canvas') // document.querySelector('#user_photo_canvas') - const ctx = canvas.getContext('2d') - ctx.fillStyle = 'black' - ctx.fillRect(0, 0, MAX_SIDE, MAX_SIDE) - const xOffset = (MAX_SIDE - resized.width) / 2 - const yOffset = (MAX_SIDE - resized.height) / 2 - ctx.drawImage(resized, xOffset, yOffset) - return canvas + // const canvas = document.createElement('canvas') // document.querySelector('#user_photo_canvas') + // const ctx = canvas.getContext('2d') + // ctx.fillStyle = 'black' + // ctx.fillRect(0, 0, MAX_SIDE, MAX_SIDE) + // const xOffset = (MAX_SIDE - resized.width) / 2 + // const yOffset = (MAX_SIDE - resized.height) / 2 + // ctx.drawImage(resized, xOffset, yOffset, resized.width, resized.height) + return resized } diff --git a/client/faceAnalysis/faceAnalysis.actions.js b/client/faceAnalysis/faceAnalysis.actions.js index 860d3292..f8d8973f 100644 --- a/client/faceAnalysis/faceAnalysis.actions.js +++ b/client/faceAnalysis/faceAnalysis.actions.js @@ -16,17 +16,20 @@ export const publicUrl = { // standard loading events const loading = (tag, offset) => ({ + ts: Date.now(), type: types.faceAnalysis.loading, tag, offset }) const loaded = (tag, data, offset = 0) => ({ + ts: Date.now(), type: types.faceAnalysis.loaded, tag, data, offset }) const polled = (data, offset = 0) => ({ + ts: Date.now(), type: types.faceAnalysis.poll, data, offset @@ -52,13 +55,19 @@ let pollTimeout = null export const poll = (payload, taskURL) => dispatch => { clearTimeout(pollTimeout) - console.log('polling...') + // console.log('polling...') get(taskURL) .then(data => { - console.log('poll', data) + // console.log('poll', data) dispatch(polled(data)) - if (data.state !== 'error' && data.state !== 'complete') { - pollTimeout = setTimeout(() => poll(payload, taskURL), POLL_DELAY) + // console.log(data.state) + if (data.state === 'COMPLETE' || data.state === 'SUCCESS') { + console.log('complete!') + } else if (data.state === 'ERROR' || data.state === 'FAILURE') { + console.log('errorr!') + dispatch(error(data)) + } else { + pollTimeout = setTimeout(() => poll(payload, taskURL)(dispatch), POLL_DELAY) } }) .catch(err => dispatch(error('result', err))) @@ -71,8 +80,8 @@ export const upload = (payload, file) => dispatch => { dispatch(loading(tag)) post(url.upload(), fd) .then(data => { - console.log('loaded!', tag, data) - dispatch(loaded(tag, data)) + // console.log('loaded!', tag, data) + dispatch(polled(tag, data)) const { result, taskURL } = data if (result && taskURL) { poll(payload, taskURL)(dispatch) diff --git a/client/faceAnalysis/faceAnalysis.container.js b/client/faceAnalysis/faceAnalysis.container.js index a86bcaa4..24848455 100644 --- a/client/faceAnalysis/faceAnalysis.container.js +++ b/client/faceAnalysis/faceAnalysis.container.js @@ -12,7 +12,7 @@ class FaceAnalysisContainer extends Component { const { payload } = this.props // console.log(payload) return ( -
+
diff --git a/client/faceAnalysis/faceAnalysis.query.js b/client/faceAnalysis/faceAnalysis.query.js index a79e3e78..33dd641f 100644 --- a/client/faceAnalysis/faceAnalysis.query.js +++ b/client/faceAnalysis/faceAnalysis.query.js @@ -19,13 +19,23 @@ class FaceAnalysisQuery extends Component { } upload(blob) { + if (this.state.image) { + URL.revokeObjectURL(this.state.image) + } + const url = URL.createObjectURL(blob) + this.setState({ image: url }) this.props.actions.upload(this.props.payload, blob) } + componentWillUnmount() { + if (this.state.image) { + URL.revokeObjectURL(this.state.image) + } + } + render() { const { result } = this.props const { image } = this.state - console.log(result) const style = {} if (image) { style.backgroundImage = 'url(' + image + ')' diff --git a/client/faceAnalysis/faceAnalysis.reducer.js b/client/faceAnalysis/faceAnalysis.reducer.js index de6e5b0a..d9be7447 100644 --- a/client/faceAnalysis/faceAnalysis.reducer.js +++ b/client/faceAnalysis/faceAnalysis.reducer.js @@ -5,25 +5,32 @@ const initialState = () => ({ task: {}, result: {}, loading: false, + startTime: 0, + timing: 0, }) export default function faceAnalysisReducer(state = initialState(), action) { + const { startTime } = state switch (action.type) { case types.faceAnalysis.loading: return { ...state, + startTime: action.ts, + timing: 0, [action.tag]: { loading: true }, } case types.faceAnalysis.loaded: return { ...state, + timing: action.ts - startTime, [action.tag]: action.data, } case types.faceAnalysis.poll: return { ...state, + timing: action.ts - startTime, result: action.data, } @@ -31,6 +38,7 @@ export default function faceAnalysisReducer(state = initialState(), action) { console.log('error', action) return { ...state, + timing: action.ts - startTime, [action.tag]: { error: action.err }, } diff --git a/client/faceAnalysis/faceAnalysis.result.js b/client/faceAnalysis/faceAnalysis.result.js index f9531eba..63a23d65 100644 --- a/client/faceAnalysis/faceAnalysis.result.js +++ b/client/faceAnalysis/faceAnalysis.result.js @@ -36,8 +36,10 @@ const errors = { class FaceAnalysisResult extends Component { render() { - const { query, task, result, loading, error } = this.props.result - console.log(this.props.result) + const { result, timing } = this.props + const { data, error, loading, message } = result + let { step, total } = data || {} + // console.log(step, total) if (loading) { return (
@@ -48,7 +50,6 @@ class FaceAnalysisResult extends Component {
) } - console.log(task, result) if (error) { // console.log(error) let errorMessage = errors[error] || errors.error @@ -56,12 +57,24 @@ class FaceAnalysisResult extends Component {
{errorMessage}
) } - if (!task && !result) return - + // console.log(result) + if (!total) { + return ( +
+ ) + } + let blurImg = data.data.blur_fn && ( +
+ + Blurred image +
+ ) return (
+ {!(step && total && message) ? '' : ({step} / {total}: {message})} + {blurImg}
- Query took {query.timing.toFixed(2)} seconds + Query took {(timing / 1000).toFixed(2)} s.
) @@ -71,6 +84,7 @@ class FaceAnalysisResult extends Component { const mapStateToProps = state => ({ query: state.faceAnalysis.query, result: state.faceAnalysis.result, + timing: state.faceAnalysis.timing, options: state.faceAnalysis.options, }) diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 663f52cc..5ad454d8 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -39,7 +39,6 @@ def show(dataset_name): else: return jsonify({ 'status': 404 }) - @api.route('/dataset//face', methods=['POST']) def upload(dataset_name): """Query an image against FAISS and return the matching identities""" diff --git a/megapixels/app/server/api_task.py b/megapixels/app/server/api_task.py index fb24c154..c9bc19ed 100644 --- a/megapixels/app/server/api_task.py +++ b/megapixels/app/server/api_task.py @@ -31,22 +31,23 @@ def task_status(task_name, task_id): return jsonify({ 'state': 'error', 'percent': 100, - 'message': 'Unknown task' + 'message': 'Unknown task', }) - # app.logger.info('task state: {}'.format(task.state)) if task.state == 'PENDING': response = { 'state': task.state, 'percent': 0, - 'message': 'Pending...' + 'message': 'Pending...', + 'data': task.info, } elif task.state != 'FAILURE': response = { 'state': task.state, 'percent': task.info.get('percent', 0), 'uuid': task.info.get('uuid', 0), - 'message': task.info.get('message', '') + 'message': task.info.get('message', ''), + 'data': task.info, } if 'result' in task.info: response['result'] = task.info['result'] @@ -56,6 +57,7 @@ def task_status(task_name, task_id): 'state': task.state, 'percent': 100, 'message': str(task.info), # this is the exception raised + 'data': task.info, } return jsonify(response) diff --git a/megapixels/app/server/tasks/blur.py b/megapixels/app/server/tasks/blur.py index d1f67f54..42977097 100644 --- a/megapixels/app/server/tasks/blur.py +++ b/megapixels/app/server/tasks/blur.py @@ -9,6 +9,8 @@ import numpy as np from app.utils.im_utils import ensure_np, ensure_pil from flask import current_app as app +import app.settings.app_cfg as cfg + from app.server.tasks import celery from celery.utils.log import get_task_logger @@ -19,57 +21,58 @@ import imutils def blur_task(self, uuid_name, fn): """Process image and update during""" celery_logger.debug('process_image_task, uuid: {}'.format(uuid_name)) + celery_logger.debug('fn: {}'.format(fn)) files = [] + meta = { + 'step': 0, + 'total': 3, + 'message': 'Starting', + 'uuid': uuid_name, + 'data': {}, + } + self.update_state(state='PROCESSING', meta=meta) + im = Image.open(fn).convert('RGB') + os.remove(fn) - self.update_state( - state = 'PROCESSING', - meta = { - 'percent': 0.25, - 'message': 'Applying blur', - 'uuid': uuid_name - }) + meta['step'] += 1 + meta['message'] = 'Applying blur' + self.update_state(state='PROCESSING', meta=meta) im_np = ensure_np(im) im_blur = cv.blur(im_np, (5,5), 1.0) im_blur_pil = ensure_pil(im_blur) fn = uuid_name + '_blur.jpg' - # fpath = os.path.join(render_dir, fn) - # im_blur_pil.save(fpath, 'JPEG', quality=95) + fpath = os.path.join(cfg.DIR_SITE_USER_CONTENT, fn) + im_blur_pil.save(fpath, 'JPEG', quality=80) + celery_logger.debug('fpath: {}'.format(fpath)) + print('fpath: {}'.format(fpath)) # files.append({ # 'title': 'Blurred image', # 'fn': render_uri + uuid_name + '_blur.jpg' # }) + meta['step'] += 1 + meta['message'] = 'Applying blur' + meta['data']['blur_fn'] = os.path.join('/user_content/', fn) + self.update_state(state='PROCESSING', meta=meta) time.sleep(3) - self.update_state( - state = 'PROCESSING', - meta = { - 'percent': 0.75, - 'message': 'Sleeping some more', - 'uuid': uuid_name - }) - time.sleep(2) + if os.path.exists(fpath): + os.remove(fpath) - data = { - 'uuid': uuid_name, - 'date': str(datetime.datetime.now()), - 'files': files - } + meta['step'] += 1 + meta['message'] = 'Securely deleting user content' + self.update_state(state='PROCESSING', meta=meta) + time.sleep(2) - # json_path = os.path.join(json_dir, uuid_name + '.json') - # with open(json_path, 'w') as json_file: - # json.dump(data, json_file) + celery_logger.debug('done!!') + + meta['step'] = meta['total'] + meta['state'] = 'complete' + return meta - celery_logger.debug('ok') - - return { - 'percent': 100, - 'state': 'complete', - 'uuid': uuid_name, - } diff --git a/megapixels/app/server/tasks/fullmonte.py b/megapixels/app/server/tasks/fullmonte.py index 17ca9403..8215656a 100644 --- a/megapixels/app/server/tasks/fullmonte.py +++ b/megapixels/app/server/tasks/fullmonte.py @@ -17,15 +17,15 @@ from app.processors import face_detector, face_landmarks from app.models.data_store import DataStore @celery.task(bind=True) -def fullmonte_task(self, uuid_name): - return - +def fullmonte_task(self, uuid_name, fn): # TOOD add selective testing opt_run_pose = True opt_run_2d_68 = True opt_run_3d_68 = True opt_run_3d_68 = True + return + # ------------------------------------------------- # init here diff --git a/megapixels/app/server/tasks/sleep.py b/megapixels/app/server/tasks/sleep.py index 9b91cc52..fa40b0e9 100644 --- a/megapixels/app/server/tasks/sleep.py +++ b/megapixels/app/server/tasks/sleep.py @@ -22,7 +22,7 @@ def sleep_task(self, uuid_name): for i,m in enumerate(msgs): percent = int(float(i)/float(len(msgs))*100.0) self.update_state( - state = 'PROCESSING', + state = 'processing', meta = { 'percent': percent, 'message': m['msg'], diff --git a/megapixels/app/settings/app_cfg.py b/megapixels/app/settings/app_cfg.py index a8f41819..fea47572 100644 --- a/megapixels/app/settings/app_cfg.py +++ b/megapixels/app/settings/app_cfg.py @@ -148,6 +148,7 @@ S3_DATASETS_PATH = "v1" # datasets is already in the filename DIR_SITE_PUBLIC = "../site/public" DIR_SITE_CONTENT = "../site/content" DIR_SITE_TEMPLATES = "../site/templates" +DIR_SITE_USER_CONTENT = "../site/public/user_content" # ----------------------------------------------------------------------------- # Celery diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index b64da4b7..e5b73562 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -25,6 +25,9 @@ font-size: 9pt; padding-top: 10px; } + +/* search results */ + .results { margin-top: 10px; padding-bottom: 10px; @@ -119,4 +122,16 @@ } .tabulator-row.tabulator-row-even { background-color: rgba(255,255,255,0.1); -} \ No newline at end of file +} + +/* analysis results */ + +.analysisContainer .result div { + width: 256px; + text-align: center; + border: 1px solid white; + padding: 10px; +} +.analysisContainer .result div img { + max-width: 100%; +} -- cgit v1.2.3-70-g09d2 From 198147bef9976a41046c3c513dc4d33babf7a238 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 22:20:06 +0100 Subject: extracting 3d facial vectors --- client/faceAnalysis/faceAnalysis.actions.js | 2 +- client/faceAnalysis/faceAnalysis.result.js | 26 ++- megapixels/app/server/api.py | 2 - megapixels/app/server/api_task.py | 13 +- megapixels/app/server/tasks/__init__.py | 15 +- megapixels/app/server/tasks/blur.py | 15 +- megapixels/app/server/tasks/demo.py | 244 ++++++++++++++++++++++++++++ megapixels/app/server/tasks/fullmonte.py | 199 ----------------------- site/assets/css/applets.css | 6 +- 9 files changed, 292 insertions(+), 230 deletions(-) create mode 100644 megapixels/app/server/tasks/demo.py delete mode 100644 megapixels/app/server/tasks/fullmonte.py diff --git a/client/faceAnalysis/faceAnalysis.actions.js b/client/faceAnalysis/faceAnalysis.actions.js index f8d8973f..2d372c1e 100644 --- a/client/faceAnalysis/faceAnalysis.actions.js +++ b/client/faceAnalysis/faceAnalysis.actions.js @@ -8,7 +8,7 @@ import { get, post } from '../util' // urls const url = { - upload: () => process.env.API_HOST + '/task/upload/blur', + upload: () => process.env.API_HOST + '/task/upload/demo', } export const publicUrl = { } diff --git a/client/faceAnalysis/faceAnalysis.result.js b/client/faceAnalysis/faceAnalysis.result.js index 63a23d65..fd079529 100644 --- a/client/faceAnalysis/faceAnalysis.result.js +++ b/client/faceAnalysis/faceAnalysis.result.js @@ -1,7 +1,6 @@ import React, { Component } from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' -import { courtesyS } from '../util' import * as actions from './faceAnalysis.actions' import { Loader } from '../common' @@ -63,16 +62,25 @@ class FaceAnalysisResult extends Component {
) } - let blurImg = data.data.blur_fn && ( -
- - Blurred image -
- ) + const results = ['blur_fn', 'landmarks_3d_68'].map(tag => { + if (tag in data.data) { + const { title, url } = data.data[tag] + return ( +
+ + {title} +
+ ) + } + return null + }).filter(a => a) + return ( -
+
{!(step && total && message) ? '' : ({step} / {total}: {message})} - {blurImg} +
+ {results} +
Query took {(timing / 1000).toFixed(2)} s.
diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 5ad454d8..b3bce9bc 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -1,5 +1,3 @@ -import logging -import logging.handlers import os import re import time diff --git a/megapixels/app/server/api_task.py b/megapixels/app/server/api_task.py index c9bc19ed..57ae9f7d 100644 --- a/megapixels/app/server/api_task.py +++ b/megapixels/app/server/api_task.py @@ -27,7 +27,8 @@ def task_status(task_name, task_id): if task_name in task_lookup: task = task_lookup[task_name]['task'].AsyncResult(task_id) # task = AsyncResult(task_id, app=celery) - else: + + if task_name not in task_lookup or task.info is None: return jsonify({ 'state': 'error', 'percent': 100, @@ -75,10 +76,16 @@ def sleep_test(): @api_task.route('/upload/blur', methods=['POST']) def upload(): + return process('blur') + +@api_task.route('/upload/demo', methods=['POST']) +def demo(): + return process('demo') + +def process(style): """ - Process a images in a particular style + Process an image in a particular style """ - style = 'blur' print('style: {}'.format(style)) if style in task_lookup: task = task_lookup[style]['task'] diff --git a/megapixels/app/server/tasks/__init__.py b/megapixels/app/server/tasks/__init__.py index fd6e398a..c0db0be5 100644 --- a/megapixels/app/server/tasks/__init__.py +++ b/megapixels/app/server/tasks/__init__.py @@ -6,6 +6,7 @@ celery = Celery(__name__, backend=cfg.CELERY_RESULT_BACKEND, broker=cfg.CELERY_B from app.server.tasks.sleep import sleep_task from app.server.tasks.blur import blur_task +from app.server.tasks.demo import demo_task def list_active_tasks(): dropdown = {} @@ -35,12 +36,12 @@ task_lookup = { 'blur': { 'title': 'Blur', 'task': blur_task, - 'active': False, + 'active': True, }, - # 'fullmonte': { - # 'title': 'TIA facial processing pipeline', - # 'task': fullmonte, - # 'active': True, - # 'default': True, - # } + 'demo': { + 'title': 'Facial processing pipeline', + 'task': demo_task, + 'active': True, + 'default': True, + } } diff --git a/megapixels/app/server/tasks/blur.py b/megapixels/app/server/tasks/blur.py index 42977097..74798cee 100644 --- a/megapixels/app/server/tasks/blur.py +++ b/megapixels/app/server/tasks/blur.py @@ -14,14 +14,14 @@ import app.settings.app_cfg as cfg from app.server.tasks import celery from celery.utils.log import get_task_logger -celery_logger = get_task_logger(__name__) +log = get_task_logger(__name__) import imutils @celery.task(bind=True) def blur_task(self, uuid_name, fn): """Process image and update during""" - celery_logger.debug('process_image_task, uuid: {}'.format(uuid_name)) - celery_logger.debug('fn: {}'.format(fn)) + log.debug('process_image_task, uuid: {}'.format(uuid_name)) + log.debug('fn: {}'.format(fn)) files = [] @@ -48,7 +48,7 @@ def blur_task(self, uuid_name, fn): fn = uuid_name + '_blur.jpg' fpath = os.path.join(cfg.DIR_SITE_USER_CONTENT, fn) im_blur_pil.save(fpath, 'JPEG', quality=80) - celery_logger.debug('fpath: {}'.format(fpath)) + log.debug('fpath: {}'.format(fpath)) print('fpath: {}'.format(fpath)) # files.append({ @@ -58,7 +58,10 @@ def blur_task(self, uuid_name, fn): meta['step'] += 1 meta['message'] = 'Applying blur' - meta['data']['blur_fn'] = os.path.join('/user_content/', fn) + meta['data']['blur_fn'] = { + 'title': 'Blurred image', + 'url': os.path.join('/user_content/', fn) + } self.update_state(state='PROCESSING', meta=meta) time.sleep(3) @@ -70,7 +73,7 @@ def blur_task(self, uuid_name, fn): self.update_state(state='PROCESSING', meta=meta) time.sleep(2) - celery_logger.debug('done!!') + log.debug('done!!') meta['step'] = meta['total'] meta['state'] = 'complete' diff --git a/megapixels/app/server/tasks/demo.py b/megapixels/app/server/tasks/demo.py new file mode 100644 index 00000000..acc5dbac --- /dev/null +++ b/megapixels/app/server/tasks/demo.py @@ -0,0 +1,244 @@ + +import app.settings.app_cfg as cfg +from app.server.tasks import celery + +from celery.utils.log import get_task_logger +log = get_task_logger(__name__) + +opt_size = (256, 256,) + +@celery.task(bind=True) +def demo_task(self, uuid_name, fn): + + import sys + import os + from os.path import join + from pathlib import Path + import time + + import numpy as np + import cv2 as cv + import dlib + from PIL import Image + import matplotlib.pyplot as plt + + from app.utils import logger_utils, file_utils, im_utils, display_utils, draw_utils + from app.utils import plot_utils + from app.processors import face_detector, face_landmarks + from app.models.data_store import DataStore + + # TODO add selective testing + opt_gpu = -1 + opt_run_pose = True + opt_run_2d_68 = True + opt_run_3d_68 = True + opt_run_3d_68 = True + paths + + meta = { + 'step': 0, + 'total': 3, + 'message': 'Starting', + 'uuid': uuid_name, + 'data': {}, + } + paths = [] + + def step(msg, step=0): + meta['step'] += step + meta['message'] = msg + log.debug('> {}'.format(msg)) + self.update_state(state='PROCESSING', meta=meta) + + step('Loading image') + self.update_state(state='PROCESSING', meta=meta) + + # os.path.join('/user_content/', fn) + + # ------------------------------------------------- + # init here + + # load image + im = cv.imread(fn) + im_resized = im_utils.resize(im, width=opt_size[0], height=opt_size[1]) + + # ---------------------------------------------------------------------------- + # detect face + + face_detector_instance = face_detector.DetectorDLIBCNN(gpu=opt_gpu) # -1 for CPU + step('Detecting face') + st = time.time() + bboxes = face_detector_instance.detect(im_resized, largest=True) + bbox = bboxes[0] + dim = im_resized.shape[:2][::-1] + bbox_dim = bbox.to_dim(dim) + if not bbox: + log.error('No face detected') + meta['error'] = 'No face detected' + self.update_state(state='FAILURE', meta=meta) + return meta + else: + log.info(f'Detected face in {(time.time() - st):.2f}s') + + + # ---------------------------------------------------------------------------- + # detect 3D landmarks + + step('Generating 3D Landmarks') + log.info('loading 3D landmark generator files...') + landmark_detector_3d_68 = face_landmarks.FaceAlignment3D_68(gpu=opt_gpu) # -1 for CPU + log.info('generating 3D landmarks...') + st = time.time() + points_3d_68 = landmark_detector_3d_68.landmarks(im_resized, bbox_dim.to_xyxy()) + log.info(f'generated 3D landmarks in {(time.time() - st):.2f}s') + log.info('') + + # draw 3d landmarks + im_landmarks_3d_68 = im_resized.copy() + draw_utils.draw_landmarks3D(im_landmarks_3d_68, points_3d_68) + draw_utils.draw_bbox(im_landmarks_3d_68, bbox_dim) + + save_image('landmarks_3d_68', '3D Landmarks', im_landmarks_3d_68) + + def save_image(key, title, data): + fn = '{}_{}.jpg'.format(uuid_name, key) + fpath = os.path.join(cfg.DIR_SITE_USER_CONTENT, fn) + paths.append(fpath) + cv.imwrite(fpath, im_landmarks_3d_68) + + meta['data']['landmarks_3d_68'] = { + 'title': '3D Landmarks', + 'url': os.path.join('/user_content/', fn), + } + step('Generated 3D Landmarks', step=0) + + # ---------------------------------------------------------------------------- + # generate 3D GIF animation + + # step('Generating GIF Animation') + # log.info('generating 3D animation...') + # if not opt_fp_out: + # fpp_im = Path(opt_fp_in) + # fp_out = join(fpp_im.parent, f'{fpp_im.stem}_anim.gif') + # else: + # fp_out = opt_fp_out + # st = time.time() + # plot_utils.generate_3d_landmark_anim(np.array(points_3d_68), fp_out, + # size=opt_gif_size, num_frames=opt_gif_frames) + # log.info(f'Generated animation in {(time.time() - st):.2f}s') + # log.info(f'Saved to: {fp_out}') + # log.info('') + + + # # ---------------------------------------------------------------------------- + # # generate face vectors, only to test if feature extraction works + + # step('Generating face vectors') + # log.info('initialize face recognition model...') + # from app.processors import face_recognition + # face_rec = face_recognition.RecognitionDLIB() + # st = time.time() + # log.info('generating face vector...') + # vec = face_rec.vec(im_resized, bbox_dim) + # log.info(f'generated face vector in {(time.time() - st):.2f}s') + # log.info('') + + + # # ---------------------------------------------------------------------------- + # # generate 68 point landmarks using dlib + + # step('Generating 2D 68PT landmarks') + # log.info('initializing face landmarks 68 dlib...') + # from app.processors import face_landmarks + # landmark_detector_2d_68 = face_landmarks.Dlib2D_68() + # log.info('generating 2D 68PT landmarks...') + # st = time.time() + # points_2d_68 = landmark_detector_2d_68.landmarks(im_resized, bbox_dim) + # log.info(f'generated 2D 68PT face landmarks in {(time.time() - st):.2f}s') + # log.info('') + + + # # ---------------------------------------------------------------------------- + # # generate pose from 68 point 2D landmarks + + # if opt_run_pose: + # step('Generating pose') + # log.info('initialize pose...') + # from app.processors import face_pose + # pose_detector = face_pose.FacePoseDLIB() + # log.info('generating pose...') + # st = time.time() + # pose_data = pose_detector.pose(points_2d_68, dim) + # log.info(f'generated pose {(time.time() - st):.2f}s') + # log.info('') + + + # # ---------------------------------------------------------------------------- + # # generate pose from 68 point 2D landmarks + + step('Done') + + # done + # self.log.debug('Add age real') + # self.log.debug('Add age apparent') + # self.log.debug('Add gender') + + + # # 3DDFA + # self.log.debug('Add depth') + # self.log.debug('Add pncc') + + # # TODO + # self.log.debug('Add 3D face model') + # self.log.debug('Add face texture flat') + # self.log.debug('Add ethnicity') + + # display + # draw bbox + + # # draw 2d landmarks + # im_landmarks_2d_68 = im_resized.copy() + # draw_utils.draw_landmarks2D(im_landmarks_2d_68, points_2d_68) + # draw_utils.draw_bbox(im_landmarks_2d_68, bbox_dim) + + # # draw pose + # if opt_run_pose: + # im_pose = im_resized.copy() + # draw_utils.draw_pose(im_pose, pose_data['point_nose'], pose_data['points']) + # draw_utils.draw_degrees(im_pose, pose_data) + + # # draw animated GIF + # im = Image.open(fp_out) + # im_frames = [] + # duration = im.info['duration'] + # try: + # while True: + # im.seek(len(im_frames)) + # mypalette = im.getpalette() + # im.putpalette(mypalette) + # im_jpg = Image.new("RGB", im.size) + # im_jpg.paste(im) + # im_np = im_utils.pil2np(im_jpg.copy()) + # im_frames.append(im_np) + # except EOFError: + # pass # end of GIF sequence + + # n_frames = len(im_frames) + # frame_number = 0 + + # # show all images here + # cv.imshow('Original', im_resized) + # cv.imshow('2D 68PT Landmarks', im_landmarks_2d_68) + # cv.imshow('3D 68PT Landmarks', im_landmarks_3d_68) + # cv.imshow('Pose', im_pose) + # cv.imshow('3D 68pt GIF', im_frames[frame_number]) + + log.debug('done!!') + + for path in paths: + if os.path.exists(path): + os.remove(path) + + meta['step'] = meta['total'] + meta['state'] = 'SUCCESS' + return meta diff --git a/megapixels/app/server/tasks/fullmonte.py b/megapixels/app/server/tasks/fullmonte.py deleted file mode 100644 index 8215656a..00000000 --- a/megapixels/app/server/tasks/fullmonte.py +++ /dev/null @@ -1,199 +0,0 @@ - -import sys -import os -from os.path import join -from pathlib import Path -import time - -import numpy as np -import cv2 as cv -import dlib -from PIL import Image -import matplotlib.pyplot as plt - -from app.utils import logger_utils, file_utils, im_utils, display_utils, draw_utils -from app.utils import plot_utils -from app.processors import face_detector, face_landmarks -from app.models.data_store import DataStore - -@celery.task(bind=True) -def fullmonte_task(self, uuid_name, fn): - # TOOD add selective testing - opt_run_pose = True - opt_run_2d_68 = True - opt_run_3d_68 = True - opt_run_3d_68 = True - - return - - # ------------------------------------------------- - # init here - - - log = logger_utils.Logger.getLogger() - - # load image - im = cv.imread(opt_fp_in) - im_resized = im_utils.resize(im, width=opt_size[0], height=opt_size[1]) - - - # ---------------------------------------------------------------------------- - # detect face - - face_detector = face_detector.DetectorDLIBCNN(gpu=opt_gpu) # -1 for CPU - log.info('detecting face...') - st = time.time() - bboxes = face_detector.detect(im_resized, largest=True) - bbox = bboxes[0] - dim = im_resized.shape[:2][::-1] - bbox_dim = bbox.to_dim(dim) - if not bbox: - log.error('no face detected') - return - else: - log.info(f'Detected face in {(time.time() - st):.2f}s') - log.info('') - - - # ---------------------------------------------------------------------------- - # detect 3D landmarks - - log.info('loading 3D landmark generator files...') - landmark_detector_3d_68 = face_landmarks.FaceAlignment3D_68(gpu=opt_gpu) # -1 for CPU - log.info('generating 3D landmarks...') - st = time.time() - points_3d_68 = landmark_detector_3d_68.landmarks(im_resized, bbox_dim.to_xyxy()) - log.info(f'generated 3D landmarks in {(time.time() - st):.2f}s') - log.info('') - - - # ---------------------------------------------------------------------------- - # generate 3D GIF animation - - log.info('generating 3D animation...') - if not opt_fp_out: - fpp_im = Path(opt_fp_in) - fp_out = join(fpp_im.parent, f'{fpp_im.stem}_anim.gif') - else: - fp_out = opt_fp_out - st = time.time() - plot_utils.generate_3d_landmark_anim(np.array(points_3d_68), fp_out, - size=opt_gif_size, num_frames=opt_gif_frames) - log.info(f'Generated animation in {(time.time() - st):.2f}s') - log.info(f'Saved to: {fp_out}') - log.info('') - - - # ---------------------------------------------------------------------------- - # generate face vectors, only to test if feature extraction works - - log.info('initialize face recognition model...') - from app.processors import face_recognition - face_rec = face_recognition.RecognitionDLIB() - st = time.time() - log.info('generating face vector...') - vec = face_rec.vec(im_resized, bbox_dim) - log.info(f'generated face vector in {(time.time() - st):.2f}s') - log.info('') - - - # ---------------------------------------------------------------------------- - # generate 68 point landmarks using dlib - - log.info('initializing face landmarks 68 dlib...') - from app.processors import face_landmarks - landmark_detector_2d_68 = face_landmarks.Dlib2D_68() - log.info('generating 2D 68PT landmarks...') - st = time.time() - points_2d_68 = landmark_detector_2d_68.landmarks(im_resized, bbox_dim) - log.info(f'generated 2D 68PT face landmarks in {(time.time() - st):.2f}s') - log.info('') - - - # ---------------------------------------------------------------------------- - # generate pose from 68 point 2D landmarks - - if opt_run_pose: - log.info('initialize pose...') - from app.processors import face_pose - pose_detector = face_pose.FacePoseDLIB() - log.info('generating pose...') - st = time.time() - pose_data = pose_detector.pose(points_2d_68, dim) - log.info(f'generated pose {(time.time() - st):.2f}s') - log.info('') - - - # ---------------------------------------------------------------------------- - # generate pose from 68 point 2D landmarks - - # done - self.log.debug('Add age real') - self.log.debug('Add age apparent') - self.log.debug('Add gender') - - - # 3DDFA - self.log.debug('Add depth') - self.log.debug('Add pncc') - - # TODO - self.log.debug('Add 3D face model') - self.log.debug('Add face texture flat') - self.log.debug('Add ethnicity') - - # display - if opt_display: - - # draw bbox - - # draw 3d landmarks - im_landmarks_3d_68 = im_resized.copy() - draw_utils.draw_landmarks3D(im_landmarks_3d_68, points_3d_68) - draw_utils.draw_bbox(im_landmarks_3d_68, bbox_dim) - - # draw 2d landmarks - im_landmarks_2d_68 = im_resized.copy() - draw_utils.draw_landmarks2D(im_landmarks_2d_68, points_2d_68) - draw_utils.draw_bbox(im_landmarks_2d_68, bbox_dim) - - # draw pose - if opt_run_pose: - im_pose = im_resized.copy() - draw_utils.draw_pose(im_pose, pose_data['point_nose'], pose_data['points']) - draw_utils.draw_degrees(im_pose, pose_data) - - # draw animated GIF - im = Image.open(fp_out) - im_frames = [] - duration = im.info['duration'] - try: - while True: - im.seek(len(im_frames)) - mypalette = im.getpalette() - im.putpalette(mypalette) - im_jpg = Image.new("RGB", im.size) - im_jpg.paste(im) - im_np = im_utils.pil2np(im_jpg.copy()) - im_frames.append(im_np) - except EOFError: - pass # end of GIF sequence - - n_frames = len(im_frames) - frame_number = 0 - - while True: - # show all images here - cv.imshow('Original', im_resized) - cv.imshow('2D 68PT Landmarks', im_landmarks_2d_68) - cv.imshow('3D 68PT Landmarks', im_landmarks_3d_68) - cv.imshow('Pose', im_pose) - cv.imshow('3D 68pt GIF', im_frames[frame_number]) - frame_number = (frame_number + 1) % n_frames - k = cv.waitKey(duration) & 0xFF - if k == 27 or k == ord('q'): # ESC - cv.destroyAllWindows() - sys.exit() - elif k != 255: - # any key to continue - break \ No newline at end of file diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index e5b73562..0c566a9f 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -126,12 +126,12 @@ /* analysis results */ -.analysisContainer .result div { +.analysisContainer .results div { width: 256px; text-align: center; - border: 1px solid white; padding: 10px; + margin: 10px; } -.analysisContainer .result div img { +.analysisContainer .results div img { max-width: 100%; } -- cgit v1.2.3-70-g09d2 From 3bd9bb2a7c3106fe69e607458d6fe4577092c1ef Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 22:42:37 +0100 Subject: displaying animated gif --- client/faceAnalysis/faceAnalysis.result.js | 4 +- megapixels/app/server/tasks/demo.py | 183 ++++++++++++----------------- 2 files changed, 75 insertions(+), 112 deletions(-) diff --git a/client/faceAnalysis/faceAnalysis.result.js b/client/faceAnalysis/faceAnalysis.result.js index fd079529..62ff174c 100644 --- a/client/faceAnalysis/faceAnalysis.result.js +++ b/client/faceAnalysis/faceAnalysis.result.js @@ -62,7 +62,9 @@ class FaceAnalysisResult extends Component {
) } - const results = ['blur_fn', 'landmarks_3d_68'].map(tag => { + + console.log(data.data) + const results = ['blur_fn', 'points_3d_68', 'landmarks_3d_68', 'landmarks_2d_68', 'pose'].map(tag => { if (tag in data.data) { const { title, url } = data.data[tag] return ( diff --git a/megapixels/app/server/tasks/demo.py b/megapixels/app/server/tasks/demo.py index acc5dbac..38a0a3c2 100644 --- a/megapixels/app/server/tasks/demo.py +++ b/megapixels/app/server/tasks/demo.py @@ -33,7 +33,9 @@ def demo_task(self, uuid_name, fn): opt_run_2d_68 = True opt_run_3d_68 = True opt_run_3d_68 = True - paths + + opt_gif_size = (256, 256,) + opt_gif_frames = 15 meta = { 'step': 0, @@ -50,6 +52,17 @@ def demo_task(self, uuid_name, fn): log.debug('> {}'.format(msg)) self.update_state(state='PROCESSING', meta=meta) + def save_image(key, title, data): + fn = '{}_{}.jpg'.format(uuid_name, key) + fpath = os.path.join(cfg.DIR_SITE_USER_CONTENT, fn) + paths.append(fpath) + cv.imwrite(fpath, data) + + meta['data'][key] = { + 'title': title, + 'url': os.path.join('/user_content/', fn), + } + step('Loading image') self.update_state(state='PROCESSING', meta=meta) @@ -100,81 +113,69 @@ def demo_task(self, uuid_name, fn): save_image('landmarks_3d_68', '3D Landmarks', im_landmarks_3d_68) - def save_image(key, title, data): - fn = '{}_{}.jpg'.format(uuid_name, key) - fpath = os.path.join(cfg.DIR_SITE_USER_CONTENT, fn) - paths.append(fpath) - cv.imwrite(fpath, im_landmarks_3d_68) - - meta['data']['landmarks_3d_68'] = { - 'title': '3D Landmarks', - 'url': os.path.join('/user_content/', fn), - } - step('Generated 3D Landmarks', step=0) - # ---------------------------------------------------------------------------- # generate 3D GIF animation - # step('Generating GIF Animation') - # log.info('generating 3D animation...') - # if not opt_fp_out: - # fpp_im = Path(opt_fp_in) - # fp_out = join(fpp_im.parent, f'{fpp_im.stem}_anim.gif') - # else: - # fp_out = opt_fp_out - # st = time.time() - # plot_utils.generate_3d_landmark_anim(np.array(points_3d_68), fp_out, - # size=opt_gif_size, num_frames=opt_gif_frames) - # log.info(f'Generated animation in {(time.time() - st):.2f}s') - # log.info(f'Saved to: {fp_out}') - # log.info('') - - - # # ---------------------------------------------------------------------------- - # # generate face vectors, only to test if feature extraction works - - # step('Generating face vectors') - # log.info('initialize face recognition model...') - # from app.processors import face_recognition - # face_rec = face_recognition.RecognitionDLIB() - # st = time.time() - # log.info('generating face vector...') - # vec = face_rec.vec(im_resized, bbox_dim) - # log.info(f'generated face vector in {(time.time() - st):.2f}s') - # log.info('') - - - # # ---------------------------------------------------------------------------- - # # generate 68 point landmarks using dlib - - # step('Generating 2D 68PT landmarks') - # log.info('initializing face landmarks 68 dlib...') - # from app.processors import face_landmarks - # landmark_detector_2d_68 = face_landmarks.Dlib2D_68() - # log.info('generating 2D 68PT landmarks...') - # st = time.time() - # points_2d_68 = landmark_detector_2d_68.landmarks(im_resized, bbox_dim) - # log.info(f'generated 2D 68PT face landmarks in {(time.time() - st):.2f}s') - # log.info('') - - - # # ---------------------------------------------------------------------------- - # # generate pose from 68 point 2D landmarks + step('Generating GIF Animation') + log.info('generating 3D animation...') + + fn = '{}_{}.gif'.format(uuid_name, '3d') + fp_out = os.path.join(cfg.DIR_SITE_USER_CONTENT, fn) + paths.append(fp_out) + + st = time.time() + plot_utils.generate_3d_landmark_anim(np.array(points_3d_68), fp_out, + size=opt_gif_size, num_frames=opt_gif_frames) + log.info(f'Generated animation in {(time.time() - st):.2f}s') + log.info(f'Saved to: {fp_out}') + log.info('') + + meta['data']['points_3d_68'] = points_3d_68 + meta['data']['points_3d_68'] = { + 'title': '3D Animated GIF', + 'url': os.path.join('/user_content/', fn), + } + + # ---------------------------------------------------------------------------- + # generate 68 point landmarks using dlib - # if opt_run_pose: - # step('Generating pose') - # log.info('initialize pose...') - # from app.processors import face_pose - # pose_detector = face_pose.FacePoseDLIB() - # log.info('generating pose...') - # st = time.time() - # pose_data = pose_detector.pose(points_2d_68, dim) - # log.info(f'generated pose {(time.time() - st):.2f}s') - # log.info('') + step('Generating 2D 68PT landmarks') + log.info('initializing face landmarks 68 dlib...') + from app.processors import face_landmarks + landmark_detector_2d_68 = face_landmarks.Dlib2D_68() + log.info('generating 2D 68PT landmarks...') + st = time.time() + points_2d_68 = landmark_detector_2d_68.landmarks(im_resized, bbox_dim) + log.info(f'generated 2D 68PT face landmarks in {(time.time() - st):.2f}s') + log.info('') + # draw 2d landmarks + im_landmarks_2d_68 = im_resized.copy() + draw_utils.draw_landmarks2D(im_landmarks_2d_68, points_2d_68) + draw_utils.draw_bbox(im_landmarks_2d_68, bbox_dim) + save_image('landmarks_2d_68', '2D Landmarks', im_landmarks_2d_68) - # # ---------------------------------------------------------------------------- - # # generate pose from 68 point 2D landmarks + # ---------------------------------------------------------------------------- + # generate pose from 68 point 2D landmarks + + if opt_run_pose: + step('Generating pose') + log.info('initialize pose...') + from app.processors import face_pose + pose_detector = face_pose.FacePoseDLIB() + log.info('generating pose...') + st = time.time() + pose_data = pose_detector.pose(points_2d_68, dim) + log.info(f'generated pose {(time.time() - st):.2f}s') + log.info('') + + im_pose = im_resized.copy() + draw_utils.draw_pose(im_pose, pose_data['point_nose'], pose_data['points']) + draw_utils.draw_degrees(im_pose, pose_data) + save_image('pose', 'Pose', im_pose) + + # ---------------------------------------------------------------------------- + # generate pose from 68 point 2D landmarks step('Done') @@ -183,7 +184,6 @@ def demo_task(self, uuid_name, fn): # self.log.debug('Add age apparent') # self.log.debug('Add gender') - # # 3DDFA # self.log.debug('Add depth') # self.log.debug('Add pncc') @@ -193,48 +193,9 @@ def demo_task(self, uuid_name, fn): # self.log.debug('Add face texture flat') # self.log.debug('Add ethnicity') - # display - # draw bbox - - # # draw 2d landmarks - # im_landmarks_2d_68 = im_resized.copy() - # draw_utils.draw_landmarks2D(im_landmarks_2d_68, points_2d_68) - # draw_utils.draw_bbox(im_landmarks_2d_68, bbox_dim) - - # # draw pose - # if opt_run_pose: - # im_pose = im_resized.copy() - # draw_utils.draw_pose(im_pose, pose_data['point_nose'], pose_data['points']) - # draw_utils.draw_degrees(im_pose, pose_data) - - # # draw animated GIF - # im = Image.open(fp_out) - # im_frames = [] - # duration = im.info['duration'] - # try: - # while True: - # im.seek(len(im_frames)) - # mypalette = im.getpalette() - # im.putpalette(mypalette) - # im_jpg = Image.new("RGB", im.size) - # im_jpg.paste(im) - # im_np = im_utils.pil2np(im_jpg.copy()) - # im_frames.append(im_np) - # except EOFError: - # pass # end of GIF sequence - - # n_frames = len(im_frames) - # frame_number = 0 - - # # show all images here - # cv.imshow('Original', im_resized) - # cv.imshow('2D 68PT Landmarks', im_landmarks_2d_68) - # cv.imshow('3D 68PT Landmarks', im_landmarks_3d_68) - # cv.imshow('Pose', im_pose) - # cv.imshow('3D 68pt GIF', im_frames[frame_number]) - log.debug('done!!') + time.sleep(3) for path in paths: if os.path.exists(path): os.remove(path) -- cgit v1.2.3-70-g09d2 From e3cfc630f52bce03c3e0e213ca93be6f748a77cb Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 22:54:09 +0100 Subject: age/gender --- client/faceAnalysis/faceAnalysis.result.js | 7 +++- megapixels/app/server/tasks/demo.py | 61 ++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/client/faceAnalysis/faceAnalysis.result.js b/client/faceAnalysis/faceAnalysis.result.js index 62ff174c..1c8a2ffb 100644 --- a/client/faceAnalysis/faceAnalysis.result.js +++ b/client/faceAnalysis/faceAnalysis.result.js @@ -64,7 +64,10 @@ class FaceAnalysisResult extends Component { } console.log(data.data) - const results = ['blur_fn', 'points_3d_68', 'landmarks_3d_68', 'landmarks_2d_68', 'pose'].map(tag => { + const results = [ + 'blur_fn', 'points_3d_68', 'landmarks_3d_68', 'landmarks_2d_68', 'pose', + 'age_real', 'age_apparent', 'gender' + ].map(tag => { if (tag in data.data) { const { title, url } = data.data[tag] return ( @@ -79,7 +82,7 @@ class FaceAnalysisResult extends Component { return (
- {!(step && total && message) ? '' : ({step} / {total}: {message})} + {!(step && total && message) ? '' : (Step {step} / {total}: {message})}
{results}
diff --git a/megapixels/app/server/tasks/demo.py b/megapixels/app/server/tasks/demo.py index 38a0a3c2..5143dd56 100644 --- a/megapixels/app/server/tasks/demo.py +++ b/megapixels/app/server/tasks/demo.py @@ -24,7 +24,7 @@ def demo_task(self, uuid_name, fn): from app.utils import logger_utils, file_utils, im_utils, display_utils, draw_utils from app.utils import plot_utils - from app.processors import face_detector, face_landmarks + from app.processors import face_detector, face_landmarks, face_age_gender from app.models.data_store import DataStore # TODO add selective testing @@ -175,14 +175,61 @@ def demo_task(self, uuid_name, fn): save_image('pose', 'Pose', im_pose) # ---------------------------------------------------------------------------- - # generate pose from 68 point 2D landmarks + # age - step('Done') + # real + age_real_predictor = face_age_gender.FaceAgeReal() + st = time.time() + age_real = age_real_predictor.predict(im_resized, bbox_dim) + log.info(f'age real took: {(time.time()-st)/1000:.5f}s') + + # apparent + age_apparent_predictor = face_age_gender.FaceAgeApparent() + st = time.time() + age_apparent = age_apparent_predictor.predict(im_resized, bbox_dim) + log.info(f'age apparent took: {(time.time()-st)/1000:.5f}s') + + # gender + gender_predictor = face_age_gender.FaceGender() + st = time.time() + gender = gender_predictor.predict(im_resized, bbox_dim) + log.info(f'gender took: {(time.time()-st)/1000:.5f}s') - # done - # self.log.debug('Add age real') - # self.log.debug('Add age apparent') - # self.log.debug('Add gender') + # ---------------------------------------------------------------------------- + # output + + log.info(f'Face coords: {bbox_dim} face') + log.info(f'Age (real): {(age_real):.2f}') + log.info(f'Age (apparent): {(age_apparent):.2f}') + log.info(f'gender: {gender}') + + + # ---------------------------------------------------------------------------- + # draw + + # draw real age + im_age_real = im_resized.copy() + draw_utils.draw_bbox(im_age_real, bbox_dim) + txt = f'{(age_real):.2f}' + draw_utils.draw_text(im_age_real, bbox_dim.pt_tl, txt) + + # apparent age + im_age_apparent = im_resized.copy() + draw_utils.draw_bbox(im_age_apparent, bbox_dim) + txt = f'{(age_apparent):.2f}' + draw_utils.draw_text(im_age_apparent, bbox_dim.pt_tl, txt) + + # gender + im_gender = im_resized.copy() + draw_utils.draw_bbox(im_age_apparent, bbox_dim) + txt = f"M: {gender['m']:.2f}, F: {gender['f']:.2f}" + draw_utils.draw_text(im_gender, (10, dim[1]-20), txt) + + save_image('age_real', 'Age (Real)', im_age_real) + save_image('age_apparent', 'Age (Apparent)', im_age_apparent) + save_image('gender', 'Gender', im_gender) + + step('Done') # # 3DDFA # self.log.debug('Add depth') -- cgit v1.2.3-70-g09d2 From 13655c42f9dd844a68f8c60a614641cdfa4c4277 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 23:23:17 +0100 Subject: displayiing all statistics --- client/faceAnalysis/faceAnalysis.actions.js | 2 +- client/faceAnalysis/faceAnalysis.result.js | 29 +++++++++-- megapixels/app/server/tasks/demo.py | 78 ++++++++++++++++------------- 3 files changed, 71 insertions(+), 38 deletions(-) diff --git a/client/faceAnalysis/faceAnalysis.actions.js b/client/faceAnalysis/faceAnalysis.actions.js index 2d372c1e..4a6fe6ed 100644 --- a/client/faceAnalysis/faceAnalysis.actions.js +++ b/client/faceAnalysis/faceAnalysis.actions.js @@ -81,7 +81,7 @@ export const upload = (payload, file) => dispatch => { post(url.upload(), fd) .then(data => { // console.log('loaded!', tag, data) - dispatch(polled(tag, data)) + dispatch(loaded(tag, data)) const { result, taskURL } = data if (result && taskURL) { poll(payload, taskURL)(dispatch) diff --git a/client/faceAnalysis/faceAnalysis.result.js b/client/faceAnalysis/faceAnalysis.result.js index 1c8a2ffb..e7a4c6de 100644 --- a/client/faceAnalysis/faceAnalysis.result.js +++ b/client/faceAnalysis/faceAnalysis.result.js @@ -66,7 +66,6 @@ class FaceAnalysisResult extends Component { console.log(data.data) const results = [ 'blur_fn', 'points_3d_68', 'landmarks_3d_68', 'landmarks_2d_68', 'pose', - 'age_real', 'age_apparent', 'gender' ].map(tag => { if (tag in data.data) { const { title, url } = data.data[tag] @@ -80,14 +79,38 @@ class FaceAnalysisResult extends Component { return null }).filter(a => a) + const statisticsLabels = ['Age (Real)', 'Age (Apparent)', 'Gender', 'Beauty score', 'Emotion'] + const statistics = [ + 'age_real', 'age_apparent', 'gender', 'beauty', 'emotion' + ].map((tag, i) => { + if (tag in data.data.statistics) { + return ( + + + {statisticsLabels[i]} + + + {data.data.statistics[tag]} + + + ) + } + return null + }).filter(a => a) + return (
- {!(step && total && message) ? '' : (Step {step} / {total}: {message})}
{results}
+ {!!statistics.length && ( + + {statistics} +
+ )}
- Query took {(timing / 1000).toFixed(2)} s. + Step {step} / {total} {message} + Query {step === total ? 'took' : 'timer:'} {(timing / 1000).toFixed(2)} s.
) diff --git a/megapixels/app/server/tasks/demo.py b/megapixels/app/server/tasks/demo.py index 5143dd56..c27b08b5 100644 --- a/megapixels/app/server/tasks/demo.py +++ b/megapixels/app/server/tasks/demo.py @@ -24,7 +24,8 @@ def demo_task(self, uuid_name, fn): from app.utils import logger_utils, file_utils, im_utils, display_utils, draw_utils from app.utils import plot_utils - from app.processors import face_detector, face_landmarks, face_age_gender + from app.processors import face_detector, face_landmarks, face_age_gender, face_beauty + # , face_emotion from app.models.data_store import DataStore # TODO add selective testing @@ -39,16 +40,16 @@ def demo_task(self, uuid_name, fn): meta = { 'step': 0, - 'total': 3, + 'total': 10, 'message': 'Starting', 'uuid': uuid_name, - 'data': {}, + 'data': { 'statistics': {} }, } paths = [] - def step(msg, step=0): - meta['step'] += step + def step(msg, step=1): meta['message'] = msg + meta['step'] += step log.debug('> {}'.format(msg)) self.update_state(state='PROCESSING', meta=meta) @@ -178,56 +179,65 @@ def demo_task(self, uuid_name, fn): # age # real + step('Running age predictor') age_real_predictor = face_age_gender.FaceAgeReal() st = time.time() age_real = age_real_predictor.predict(im_resized, bbox_dim) log.info(f'age real took: {(time.time()-st)/1000:.5f}s') + meta['data']['statistics']['age_real'] = f'{(age_real):.2f}' # apparent age_apparent_predictor = face_age_gender.FaceAgeApparent() st = time.time() age_apparent = age_apparent_predictor.predict(im_resized, bbox_dim) log.info(f'age apparent took: {(time.time()-st)/1000:.5f}s') + meta['data']['statistics']['age_apparent'] = f'{(age_apparent):.2f}' # gender + step('Running gender predictor') gender_predictor = face_age_gender.FaceGender() st = time.time() gender = gender_predictor.predict(im_resized, bbox_dim) log.info(f'gender took: {(time.time()-st)/1000:.5f}s') + meta['data']['statistics']['gender'] = f"M: {gender['m']:.2f}, F: {gender['f']:.2f}" - # ---------------------------------------------------------------------------- - # output - - log.info(f'Face coords: {bbox_dim} face') - log.info(f'Age (real): {(age_real):.2f}') - log.info(f'Age (apparent): {(age_apparent):.2f}') - log.info(f'gender: {gender}') - + # # ---------------------------------------------------------------------------- + # # emotion - # ---------------------------------------------------------------------------- - # draw + # emotion_predictor = face_emotion.FaceEmotion(gpu=opt_gpu) + # emotion_score = emotion_predictor.emotion(im_resized, bbox_dim) + # log.info(f'emotion score: {(100*emotion_score):.2f}') - # draw real age - im_age_real = im_resized.copy() - draw_utils.draw_bbox(im_age_real, bbox_dim) - txt = f'{(age_real):.2f}' - draw_utils.draw_text(im_age_real, bbox_dim.pt_tl, txt) + # im_emotion = im_resized.copy() + # draw_utils.draw_bbox(im_emotion, bbox_dim) + # txt = f'emotion score: {(100*emotion_score):.2f}' + # draw_utils.draw_text(im_emotion, bbox_dim.pt_tl, txt) + # save_image('emotion', 'Emotion', im_emotion) - # apparent age - im_age_apparent = im_resized.copy() - draw_utils.draw_bbox(im_age_apparent, bbox_dim) - txt = f'{(age_apparent):.2f}' - draw_utils.draw_text(im_age_apparent, bbox_dim.pt_tl, txt) - # gender - im_gender = im_resized.copy() - draw_utils.draw_bbox(im_age_apparent, bbox_dim) - txt = f"M: {gender['m']:.2f}, F: {gender['f']:.2f}" - draw_utils.draw_text(im_gender, (10, dim[1]-20), txt) - - save_image('age_real', 'Age (Real)', im_age_real) - save_image('age_apparent', 'Age (Apparent)', im_age_apparent) - save_image('gender', 'Gender', im_gender) + # ---------------------------------------------------------------------------- + # beauty + + # TODO fix Keras CPU/GPU device selection issue + # NB: GPU visibility issues with dlib/keras + # Wrap this with cuda toggle and run before init dlib GPU + + step('Running beauty predictor') + device_cur = os.getenv('CUDA_VISIBLE_DEVICES', '') + os.environ['CUDA_VISIBLE_DEVICES'] = '' + beauty_predictor = face_beauty.FaceBeauty() + os.environ['CUDA_VISIBLE_DEVICES'] = device_cur + + beauty_score = beauty_predictor.beauty(im_resized, bbox_dim) + log.info(f'beauty score: {(100*beauty_score):.2f}') + + # # draw 2d landmarks + # im_beauty = im_resized.copy() + # draw_utils.draw_bbox(im_beauty, bbox_dim) + # txt = f'Beauty score: {(100*beauty_score):.2f}' + # draw_utils.draw_text(im_beauty, bbox_dim.pt_tl, txt) + # save_image('beauty', 'Beauty', im_beauty) + meta['data']['statistics']['beauty'] = f'{(100*beauty_score):.2f}' step('Done') -- cgit v1.2.3-70-g09d2 From 208139ce2be2b19170ceb9ea4acb00246bb96e83 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 23:25:36 +0100 Subject: face analysis --- client/faceAnalysis/faceAnalysis.result.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/faceAnalysis/faceAnalysis.result.js b/client/faceAnalysis/faceAnalysis.result.js index e7a4c6de..9d77f258 100644 --- a/client/faceAnalysis/faceAnalysis.result.js +++ b/client/faceAnalysis/faceAnalysis.result.js @@ -109,7 +109,7 @@ class FaceAnalysisResult extends Component { )}
- Step {step} / {total} {message} + Step {step} / {total} {message}
Query {step === total ? 'took' : 'timer:'} {(timing / 1000).toFixed(2)} s.
-- cgit v1.2.3-70-g09d2 From a712efd526481ad3743c860c1fb142889bb33cf4 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 23:51:23 +0100 Subject: html --- site/public/test/index.html | 16 ++++++++-------- site/public/test/style/index.html | 9 +++------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/site/public/test/index.html b/site/public/test/index.html index 41f8eda5..b4d16036 100644 --- a/site/public/test/index.html +++ b/site/public/test/index.html @@ -30,14 +30,14 @@

Megapixels UI Tests

diff --git a/site/public/test/style/index.html b/site/public/test/style/index.html index f25f1daf..2a41b8b1 100644 --- a/site/public/test/style/index.html +++ b/site/public/test/style/index.html @@ -30,7 +30,7 @@

Style Examples

← Back to test index

-
Style Guide Test
Style Guide Test
Date
17-Jan-2019
Numbers
17
Identities
12,139
But also
This is a test of the stylesheet

Header 1

+
Style Guide Test
Style Guide Test
Date
17-Jan-2019
Numbers
17
Identities
12,139
But also
This is a test of the stylesheet

Header 1

Header 2

Header 3

Header 4

@@ -53,17 +53,14 @@
Person 2
Person 2
Person 3. Let me tell you about Person 3.  This person has a very long description with text which wraps like crazy
Person 3. Let me tell you about Person 3. This person has a very long description with text which wraps like crazy

est, qui dolorem ipsum, quia dolor sit amet consectetur adipisci[ng] velit, sed quia non-numquam [do] eius modi tempora inci[di]dunt, ut labore et dolore magnam aliquam quaerat voluptatem.

-
This image is extremely wide and the text beneath it will wrap but thats fine because it can also contain <a href="https://example.com/">hyperlinks</a>! Yes, you read that right—hyperlinks! Lorem ipsum dolor sit amet ad volotesque sic hoc ad nauseam
This image is extremely wide and the text beneath it will wrap but that's fine because it can also contain hyperlinks! Yes, you read that right—hyperlinks! Lorem ipsum dolor sit amet ad volotesque sic hoc ad nauseam

Inline code has back-ticks around it.

+
This image is extremely wide and the text beneath it will wrap but thats fine because it can also contain <a href="https://example.com/">hyperlinks</a>! Yes, you read that right—hyperlinks! Lorem ipsum dolor sit amet ad volotesque sic hoc ad nauseam
This image is extremely wide and the text beneath it will wrap but that's fine because it can also contain hyperlinks! Yes, you read that right—hyperlinks! Lorem ipsum dolor sit amet ad volotesque sic hoc ad nauseam

Inline code has back-ticks around it.

var s = "JavaScript syntax highlighting";
 alert(s);
 
s = "Python syntax highlighting"
 print(s)
 
-
Generic code block. Note that code blocks that are not so marked will not appear.
-But let's throw in a <b>tag</b>.
-
-

Horizontal rule

+
tag."]}'>

Horizontal rule


Citations below here

-- cgit v1.2.3-70-g09d2 From 3b2f0dc6d969fa323fe8775b4269e17c60192431 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 23:57:40 +0100 Subject: readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index aa6e3335..67422ee7 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ pip install requests simplejson click pdfminer.six pip install urllib3 flask flask_sqlalchemy mysql-connector pip install pymediainfo tqdm opencv-python imutils pip install scikit-image python-dotenv imagehash scikit-learn colorlog +pip install celery keras tensorflow sudo apt-get install libmysqlclient-dev -- cgit v1.2.3-70-g09d2