From e1fba831b7c22f9840c5e92227f688079b9a206e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Fri, 14 Dec 2018 15:50:07 +0100 Subject: mysql import script --- megapixels/app/models/sql_factory.py | 91 ++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 megapixels/app/models/sql_factory.py (limited to 'megapixels/app/models/sql_factory.py') diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py new file mode 100644 index 00000000..4adc6f48 --- /dev/null +++ b/megapixels/app/models/sql_factory.py @@ -0,0 +1,91 @@ +import os + +from sqlalchemy import create_engine, Table, Column, String, Integer, DateTime, Float +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.ext.declarative import AbstractConcreteBase, ConcreteBase + +connection_url = "mysql+mysqldb://{}:{}@{}/{}".format( + os.getenv("DB_USER"), + os.getenv("DB_PASS"), + os.getenv("DB_HOST"), + os.getenv("DB_NAME") +) + +engine = create_engine(connection_url) +Session = sessionmaker(bind=engine) +session = Session() +Base = declarative_base(engine) + +class SqlDataset: + def __init__(self, name): + self.name = name + self.tables = {} + + def get_table(self, type): + if type in self.tables: + return self.tables[type] + elif type == 'uuid': + self.tables[type] = self.uuid_table() + elif type == 'roi': + self.tables[type] = self.roi_table() + elif type == 'identity_meta': + self.tables[type] = self.identity_table() + elif type == 'pose': + self.tables[type] = self.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(Base): + __tablename__ = self.name + "_uuid" + id = Column(Integer, primary_key=True) + uuid = Column(String(36), nullable=False) + return UUID + + # ==> 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(Base): + __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) + image_width = Column(Integer, nullable=False) + w = Column(Float, nullable=False) + x = Column(Float, nullable=False) + y = Column(Float, nullable=False) + 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(Base): + __tablename__ = self.name + "_identity" + id = Column(Integer, primary_key=True) + fullname = Column(String(36), nullable=False) + description = Column(String(36), nullable=False) + gender = Column(String(1), nullable=False) + images = Column(Integer, nullable=False) + image_id = Column(Integer, nullable=False) + return Identity + + # ==> pose.csv <== + # index,image_index,pitch,roll,yaw + # 0,0,11.16264458441435,10.415885631337728,22.99719032415318 + def pose_table(self): + class Pose(Base): + __tablename__ = self.name + "_pose" + id = Column(Integer, primary_key=True) + image_id = Column(Integer, primary_key=True) + pitch = Column(Float, nullable=False) + roll = Column(Float, nullable=False) + yaw = Column(Float, nullable=False) + return Pose -- cgit v1.2.3-70-g09d2 From b39b1d51db2d485e9c60fb4d3f5445474cef8700 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Fri, 14 Dec 2018 16:39:47 +0100 Subject: mysql import functions --- megapixels/app/models/sql_factory.py | 30 +++++++++------ megapixels/commands/faiss/build.py | 62 ------------------------------ megapixels/commands/faiss/build_faiss.py | 58 ++++++++++++++++++++++++++++ megapixels/commands/faiss/sync.py | 18 --------- megapixels/commands/faiss/sync_metadata.py | 18 +++++++++ 5 files changed, 95 insertions(+), 91 deletions(-) delete mode 100644 megapixels/commands/faiss/build.py create mode 100644 megapixels/commands/faiss/build_faiss.py delete mode 100644 megapixels/commands/faiss/sync.py create mode 100644 megapixels/commands/faiss/sync_metadata.py (limited to 'megapixels/app/models/sql_factory.py') diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 4adc6f48..ecca0c7f 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -2,8 +2,7 @@ import os from sqlalchemy import create_engine, Table, Column, String, Integer, DateTime, Float from sqlalchemy.orm import sessionmaker -from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy.ext.declarative import AbstractConcreteBase, ConcreteBase +from sqlalchemy.ext.declarative import declarative_base connection_url = "mysql+mysqldb://{}:{}@{}/{}".format( os.getenv("DB_USER"), @@ -12,15 +11,24 @@ connection_url = "mysql+mysqldb://{}:{}@{}/{}".format( os.getenv("DB_NAME") ) -engine = create_engine(connection_url) -Session = sessionmaker(bind=engine) -session = Session() -Base = declarative_base(engine) +# Session = sessionmaker(bind=engine) +# session = Session() + class SqlDataset: - def __init__(self, name): + """ + Bridge between the facial information CSVs connected to the datasets, and MySQL + - each dataset should have files that can be loaded into these database models + - names will be fixed to work in SQL (index -> id) + - we can then have more generic models for fetching this info after doing a FAISS query + """ + def __init__(self, name, base_model=None): self.name = name self.tables = {} + if base_model is None: + engine = create_engine(connection_url) + base_model = declarative_base(engine) + self.base_model = base_model def get_table(self, type): if type in self.tables: @@ -41,7 +49,7 @@ class SqlDataset: # index,uuid # 0,f03fd921-2d56-4e83-8115-f658d6a72287 def uuid_table(self): - class UUID(Base): + class UUID(self.base_model): __tablename__ = self.name + "_uuid" id = Column(Integer, primary_key=True) uuid = Column(String(36), nullable=False) @@ -51,7 +59,7 @@ class SqlDataset: # 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(Base): + class ROI(self.base_model): __tablename__ = self.name + "_roi" id = Column(Integer, primary_key=True) h = Column(Float, nullable=False) @@ -67,7 +75,7 @@ class SqlDataset: # index,fullname,description,gender,images,image_index # 0,A. J. Cook,Canadian actress,f,1,0 def identity_table(self): - class Identity(Base): + class Identity(self.base_model): __tablename__ = self.name + "_identity" id = Column(Integer, primary_key=True) fullname = Column(String(36), nullable=False) @@ -81,7 +89,7 @@ class SqlDataset: # index,image_index,pitch,roll,yaw # 0,0,11.16264458441435,10.415885631337728,22.99719032415318 def pose_table(self): - class Pose(Base): + class Pose(self.base_model): __tablename__ = self.name + "_pose" id = Column(Integer, primary_key=True) image_id = Column(Integer, primary_key=True) diff --git a/megapixels/commands/faiss/build.py b/megapixels/commands/faiss/build.py deleted file mode 100644 index e525542a..00000000 --- a/megapixels/commands/faiss/build.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Index all of the FAISS datasets -""" - -import os -import glob -import click -import faiss -import time -import numpy as np - -from app.utils.file_utils import load_recipe, load_csv_safe -from app.settings import app_cfg as cfg - -engine = create_engine('sqlite:///:memory:') - -class DefaultRecipe: - def __init__(self): - self.dim = 128 - self.factory_type = 'Flat' - -@click.command() -@click.pass_context -def cli(ctx): - """build the FAISS index. - - looks for all datasets in faiss/metadata/ - - uses the recipe above by default - - however you can override this by adding a new recipe in faiss/recipes/{name}.json - """ - datasets = [] - for fn in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): - name = os.path.basename(fn) - recipe_fn = os.path.join(cfg.DIR_FAISS_RECIPES, name + ".json") - if os.path.exists(recipe_fn): - build_faiss(name, load_recipe(recipe_fn)) - else: - build_faiss(name, DefaultRecipe()) - # index identities - # certain CSV files should be loaded into mysql - # User.__table__.drop() - SQLemployees.create(engine) - -def build_faiss(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") - - index = faiss.index_factory(recipe.dim, recipe.factory_type) - - keys, rows = load_csv_safe(vec_fn) - feats = np.array([ list(map(float, row[3].split(","))) for row in rows ]).astype('float32') - n, d = feats.shape - - print("{}: training {} x {} dim vectors".format(name, n, d)) - print(recipe.factory_type) - - add_start = time.time() - index.add(feats) - add_end = time.time() - add_time = add_end - add_start - print("{}: add time: {:.1f}s".format(name, add_time)) - - faiss.write_index(index, index_fn) diff --git a/megapixels/commands/faiss/build_faiss.py b/megapixels/commands/faiss/build_faiss.py new file mode 100644 index 00000000..96d3f99e --- /dev/null +++ b/megapixels/commands/faiss/build_faiss.py @@ -0,0 +1,58 @@ +""" +Index all of the FAISS datasets +""" + +import os +import glob +import click +import faiss +import time +import numpy as np + +from app.utils.file_utils import load_recipe, load_csv_safe +from app.settings import app_cfg as cfg + +engine = create_engine('sqlite:///:memory:') + +class DefaultRecipe: + def __init__(self): + self.dim = 128 + self.factory_type = 'Flat' + +@click.command() +@click.pass_context +def cli(ctx): + """build the FAISS index. + - looks for all datasets in faiss/metadata/ + - uses the recipe above by default + - however you can override this by adding a new recipe in faiss/recipes/{name}.json + """ + datasets = [] + for fn in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): + name = os.path.basename(fn) + recipe_fn = os.path.join(cfg.DIR_FAISS_RECIPES, name + ".json") + if os.path.exists(recipe_fn): + build_faiss(name, load_recipe(recipe_fn)) + else: + build_faiss(name, DefaultRecipe()) + +def build_faiss(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") + + index = faiss.index_factory(recipe.dim, recipe.factory_type) + + keys, rows = load_csv_safe(vec_fn) + feats = np.array([ list(map(float, row[3].split(","))) for row in rows ]).astype('float32') + n, d = feats.shape + + print("{}: training {} x {} dim vectors".format(name, n, d)) + print(recipe.factory_type) + + add_start = time.time() + index.add(feats) + add_end = time.time() + add_time = add_end - add_start + print("{}: add time: {:.1f}s".format(name, add_time)) + + faiss.write_index(index, index_fn) diff --git a/megapixels/commands/faiss/sync.py b/megapixels/commands/faiss/sync.py deleted file mode 100644 index b01211b4..00000000 --- a/megapixels/commands/faiss/sync.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Sync the FAISS metadata -""" - -import subprocess -import click - -from app.settings import app_cfg as cfg - -@click.command() -@click.pass_context -def cli(ctx): - """synchronize metadata files from s3""" - sts = subprocess.call([ - "s3cmd", "sync", - "s3://megapixels/v1/metadata/", - cfg.DIR_FAISS_METADATA + '/', - ]) diff --git a/megapixels/commands/faiss/sync_metadata.py b/megapixels/commands/faiss/sync_metadata.py new file mode 100644 index 00000000..b01211b4 --- /dev/null +++ b/megapixels/commands/faiss/sync_metadata.py @@ -0,0 +1,18 @@ +""" +Sync the FAISS metadata +""" + +import subprocess +import click + +from app.settings import app_cfg as cfg + +@click.command() +@click.pass_context +def cli(ctx): + """synchronize metadata files from s3""" + sts = subprocess.call([ + "s3cmd", "sync", + "s3://megapixels/v1/metadata/", + cfg.DIR_FAISS_METADATA + '/', + ]) -- cgit v1.2.3-70-g09d2 From 36b6082dfa768cbf35d40dc2c82706dfae0b687b Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Fri, 14 Dec 2018 17:24:23 +0100 Subject: flask server click script --- faiss/__init__.py | 0 faiss/requirements.txt | 11 + faiss/run.sh | 1 + faiss/server.py | 68 +++++ faiss/static/css/app.css | 289 ++++++++++++++++++++ faiss/static/favicon.ico | Bin 0 -> 15086 bytes faiss/static/img/play.png | Bin 0 -> 1231 bytes faiss/static/index.html | 83 ++++++ faiss/static/js/app.js | 491 ++++++++++++++++++++++++++++++++++ faiss/static/js/dataUriToBlob.js | 58 ++++ faiss/static/js/metadata-app.js | 50 ++++ faiss/static/js/store2.min.js | 5 + faiss/static/metadata.html | 11 + faiss/static/search.html | 1 + faiss/util.py | 29 ++ faiss/wsgi.py | 5 + megapixels/app/models/sql_factory.py | 1 - megapixels/app/server/api/image.py | 40 +++ megapixels/app/server/create.py | 27 ++ megapixels/app/server/static | 1 + megapixels/cli_flask.py | 19 ++ megapixels/commands/faiss/build_db.py | 21 +- 22 files changed, 1203 insertions(+), 8 deletions(-) create mode 100644 faiss/__init__.py create mode 100644 faiss/requirements.txt create mode 100644 faiss/run.sh create mode 100644 faiss/server.py create mode 100644 faiss/static/css/app.css create mode 100644 faiss/static/favicon.ico create mode 100644 faiss/static/img/play.png create mode 100644 faiss/static/index.html create mode 100644 faiss/static/js/app.js create mode 100644 faiss/static/js/dataUriToBlob.js create mode 100644 faiss/static/js/metadata-app.js create mode 100644 faiss/static/js/store2.min.js create mode 100644 faiss/static/metadata.html create mode 100644 faiss/static/search.html create mode 100644 faiss/util.py create mode 100644 faiss/wsgi.py create mode 100644 megapixels/app/server/api/image.py create mode 100644 megapixels/app/server/create.py create mode 120000 megapixels/app/server/static create mode 100644 megapixels/cli_flask.py (limited to 'megapixels/app/models/sql_factory.py') diff --git a/faiss/__init__.py b/faiss/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/faiss/requirements.txt b/faiss/requirements.txt new file mode 100644 index 00000000..1d60aabc --- /dev/null +++ b/faiss/requirements.txt @@ -0,0 +1,11 @@ +Pillow +h5py +tensorflow +Keras +Flask +opencv-python +imagehash +scikit-image +scikit-learn +imutils + diff --git a/faiss/run.sh b/faiss/run.sh new file mode 100644 index 00000000..8f9e77e2 --- /dev/null +++ b/faiss/run.sh @@ -0,0 +1 @@ +uwsgi --http 127.0.0.1:5000 --file wsgi.py --callable app --processes 1 diff --git a/faiss/server.py b/faiss/server.py new file mode 100644 index 00000000..a8c660fa --- /dev/null +++ b/faiss/server.py @@ -0,0 +1,68 @@ +#!python + +import os +import sys +import json +import time +import argparse +import cv2 as cv +import numpy as np +from datetime import datetime +from flask import Flask, request, render_template, jsonify +from PIL import Image # todo: try to remove PIL dependency +import re + +sanitize_re = re.compile('[\W]+') +valid_exts = ['.gif', '.jpg', '.jpeg', '.png'] + +from dotenv import load_dotenv +load_dotenv() + +from feature_extractor import FeatureExtractor + +DEFAULT_LIMIT = 50 + +app = Flask(__name__, static_url_path="/search/static", static_folder="static") + +# static api routes - this routing is actually handled in the JS +@app.route('/', methods=['GET']) +def index(): + return app.send_static_file('metadata.html') + +# search using an uploaded file +@app.route('/search/api/upload', methods=['POST']) +def upload(): + file = request.files['query_img'] + fn = file.filename + if fn.endswith('blob'): + fn = 'filename.jpg' + + basename, ext = os.path.splitext(fn) + print("got {}, type {}".format(basename, ext)) + if ext.lower() not in valid_exts: + return jsonify({ 'error': 'not an image' }) + + uploaded_fn = datetime.now().isoformat() + "_" + basename + uploaded_fn = sanitize_re.sub('', uploaded_fn) + uploaded_img_path = "static/uploaded/" + uploaded_fn + ext + uploaded_img_path = uploaded_img_path.lower() + print('query: {}'.format(uploaded_img_path)) + + img = Image.open(file.stream).convert('RGB') + # img.save(uploaded_img_path) + # vec = db.load_feature_vector_from_file(uploaded_img_path) + vec = fe.extract(img) + # print(vec.shape) + + results = db.search(vec, limit=limit) + query = { + 'timing': time.time() - start, + } + print(results) + return jsonify({ + 'results': results, + }) + +if __name__=="__main__": + app.run("0.0.0.0", debug=False) + diff --git a/faiss/static/css/app.css b/faiss/static/css/app.css new file mode 100644 index 00000000..a3b24736 --- /dev/null +++ b/faiss/static/css/app.css @@ -0,0 +1,289 @@ +/* css boilerplate */ + +* { box-sizing: border-box; } +html,body { + margin: 0; padding: 0; + width: 100%; height: 100%; +} +body { + font-family: Helvetica, sans-serif; + font-weight: 300; + padding-top: 60px; +} + +/* header */ + +header { + position: fixed; + top: 0; + left: 0; + height: 60px; + width: 100%; + background: #11f; + color: white; + align-items: stretch; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + z-index: 3; +} +header > section { + justify-content: flex-start; + align-items: center; + display: flex; + flex: 1 0; + font-weight: bold; +} +header > section:last-of-type { + justify-content: flex-end; +} +header a { + color: hsla(0,0%,100%,.89); + text-decoration: none; + line-height: 18px; + font-size: 14px; + font-weight: 700; + padding: .35rem .4rem; + white-space: nowrap; +} +header .logged-in { + font-size: 12px; + font-weight: normal; + padding: 0 0.5rem; +} +header .logout { + padding: 0 6px; + border-left: 1px solid #99f; +} +header .logout a { + font-size: 12px; +} +.menuToggle { + width: 30px; + height: 30px; + margin: 5px; + cursor: pointer; + line-height: 1; +} + +/* form at the top */ + +#form { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 20px; + padding: 20px; + border: 1px solid #ddd; +} +input[type=text] { + border: 1px solid #888; + padding: 4px; + font-size: 15px; +} +input[type=file] { + max-width: 200px; + border-radius: 2px; +} +input[type=file]:invalid + button { visibility: hidden!important; } +input[type=file]:valid + button { visibility: visible!important; } +#form > div { + display: flex; + flex-direction: row; + align-items: center; +} +#form > div * { + margin: 0 3px; +} + +/* saving UI form */ + +label { + display: block; + white-space: nowrap; + padding-bottom: 10px; +} +label:last-child { + padding-bottom: 0; +} +label span { + display: inline-block; + min-width: 80px; +} +.saving_ui { + display: none; +} +.saving .saving_ui { + display: flex; + border: 1px solid #ddd; + margin: 20px; + padding: 20px; + flex-direction: row; + justify-content: space-between; +} + +/* query box, shows either searched image, directory name, etc */ + +.loading .results, +.prefetch .query, .prefetch .results, +.browsing .score, .browsing .browse, +.photo .browse, +.saving .score { + display: none; +} +.browsing .query div { display: inline; margin-left: 5px; font-weight: bold; } +.saving .query div { display: inline; margin-left: 5px; font-weight: bold; } +.load_message { + opacity: 0; +} +.loading .load_message { + display: block; + margin: 20px; + font-weight: bold; +} + +.query { + margin: 20px; +} +.query > div { + margin-top: 10px; + position: relative; + display: flex; + flex-direction: row; + align-items: flex-start; +} +.query img { + cursor: crosshair; + max-width: 400px; + display: block; +} +.query > div > .box { + position: absolute; + border: 1px solid #11f; + background: rgba(17,17,255,0.1); + pointer-events: none; +} +.query canvas { + margin-left: 20px; + max-width: 200px; +} + +/* search results */ + +.results { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} +.results > div { + display: flex; + flex-direction: column; + justify-content: flex-end; + width: 210px; + margin: 15px; + padding: 5px; + border: 1px solid transparent; +} +.results > div.saved { + border-radius: 2px; + background: #fafaaa; +} +.results > div img { + cursor: pointer; + max-width: 210px; + margin-bottom: 10px; +} +.results > div > div { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} +.results a:visited .btn { + color: #99d; +} +.score { + font-size: 12px; + color: #444; +} + + +/* spinner */ + +.loader { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; + background: rgba(255,255,255,0.9); +} +.loader > div { + background: white; + padding: 20px; + box-shadow: 0 1px 2px #bbb; + border-radius: 2px; +} +.spinner { + position: relative; + width: 32px; + height: 32px; + color: #11f; + margin: 0 auto; +} +.spinner:after { + position: absolute; + margin: auto; + width: 100%; + height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + content: " "; + display: inline-block; + border-radius: 50%; + border-style: solid; + border-width: 0.15em; + -webkit-background-clip: padding-box; + border-color: currentColor currentColor currentColor transparent; + box-sizing: border-box; + -webkit-animation: ld-cycle 0.7s infinite linear; + animation: ld-cycle 0.7s infinite linear; +} +@-webkit-keyframes ld-cycle { + 0%, 50%, 100% { + animation-timing-function: cubic-bezier(0.5, 0.5, 0.5, 0.5); + } + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 50% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes ld-cycle { + 0%, 50%, 100% { + animation-timing-function: cubic-bezier(0.5, 0.5, 0.5, 0.5); + } + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + 50% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} diff --git a/faiss/static/favicon.ico b/faiss/static/favicon.ico new file mode 100644 index 00000000..d97f2f59 Binary files /dev/null and b/faiss/static/favicon.ico differ diff --git a/faiss/static/img/play.png b/faiss/static/img/play.png new file mode 100644 index 00000000..40f76045 Binary files /dev/null and b/faiss/static/img/play.png differ diff --git a/faiss/static/index.html b/faiss/static/index.html new file mode 100644 index 00000000..cf59c628 --- /dev/null +++ b/faiss/static/index.html @@ -0,0 +1,83 @@ + + + + + + + +VFrame Image Import + + + +
+ + + +
+ +
+ +
+
+ + +
+
+ + + +
+
+ +
+
+ + + + +
+
+ +
+ +
+
+ +
+
+ + +
+ + + + + + + + diff --git a/faiss/static/js/app.js b/faiss/static/js/app.js new file mode 100644 index 00000000..77164c76 --- /dev/null +++ b/faiss/static/js/app.js @@ -0,0 +1,491 @@ +/* eslint no-use-before-define: 0, camelcase: 0, one-var-declaration-per-line: 0, one-var: 0, quotes: 0, prefer-destructuring: 0, no-alert: 0, no-console: 0, no-multi-assign: 0 */ + +function loadApp() { + const result_template = document.querySelector('#result-template').innerHTML + const results_el = document.querySelector('.results') + const query_div = document.body.querySelector('.query > div') + let bounds + let token, username + let x, y, mouse_x, mouse_y, dx, dy, box + let dragging = false + let cropping = false + let creating = false + let did_check = false + + function init() { + login() + bind() + route() + } + function bind() { + window.onpopstate = route + document.querySelector('[name=img]').addEventListener('change', upload) + on('click', '.results a', preventDefault) + on('click', '.search', search) + on('click', '.panic', panic) + on('click', '.upload_again', upload_again) + on('click', '.browse', browse) + on('click', '.results img', save) + on('click', '.view_saved', loadSaved) + on('click', '.create_new_group', createNewGroup) + on('click', '.reset', reset) + on('click', '.random', random) + on('click', '.check', check) + on('mousedown', '.query img', down) + window.addEventListener('mousemove', move) + window.addEventListener('mouseup', up) + window.addEventListener('keydown', keydown) + } + function route() { + const path = window.location.pathname.split('/') + // remove initial slash + path.shift() + // remove dummy route + if (path[0] === 'search') path.shift() + switch (path[0]) { + case 'fetch': + search({ target: { url: window.location.search.substr(1).split('=')[1] } }) + break + case 'view': + search(path.slice(1)) + break + case 'q': + if (path.length === 3) { + search({ target: { dir: path[1], fn: path[2] } }) + } else { + browse({ target: { dir: path[1], fn: null } }) + } + break + case 'saved': + loadSaved() + break + default: + break + } + } + function keydown(e) { + switch (e.keyCode) { + case 27: // escape + panic() + break + default: + break + } + } + + // load search results + function loadResults(data) { + console.log(data) + if (!data.query.url) return + // console.log(data) + document.body.className = 'searching' + const path = getPathFromImage(data.query.url) + pushState('searching', "/search/fetch/?url=" + path.url) + if (path.dir === 'uploaded' && path.fn.match('_filename')) { + loadMessage( + "< Back | " + + "Searching subregion, " + + "found " + data.results.length + " images" + ) + } else { + loadMessage( + "Found " + data.results.length + " images" + ) + } + loadQuery(data.query.url) + if (!data.results.length) { + results_el.innerHTML = "No results" + return + } + const saved = window.store.get('saved', []) + + results_el.innerHTML = data.results.map(res => { + const { distance, file, hash, frame, url } = res + const isSaved = saved.indexOf(url) !== -1 + const { type } = getPathFromImage(url) + let className = isSaved ? 'saved' : '' + className += ' ' + type + let t = result_template + .replace('{score}', Math.floor(clamp(1 - distance, 0, 1) * 100) + "%") + .replace('{browse}', '/search/q/' + hash) + .replace('{search}', '/search/view/' + [file, hash, frame].join('/')) + .replace('{metadata}', '/metadata/' + hash) + .replace('{className}', className) + .replace('{saved_msg}', isSaved ? 'Saved' : 'Save') + .replace('{img}', url) + return t + }).join('') + } + + function loadDirectory(data) { + console.log(data) + document.body.className = 'browsing' + pushState('searching', "/search/q/" + data.path) + loadMessage("Video: " + data.path + "") + loadQuery("") + if (!data.results.length) { + results_el.innerHTML = "No frames found" + return + } + const saved = window.store.get('saved', []) + results_el.innerHTML = data.results + .map(result => [parseInt(result.frame, 10), result]) + .sort((a, b) => a[0] - b[0]) + .map(pair => { + let { file, hash, frame, url } = pair[1] + const isSaved = saved.indexOf(url) !== -1 + let className = isSaved ? 'saved' : '' + let t = result_template + .replace('{img}', url) + .replace('{browse}', '/search/q/' + hash) + .replace('{search}', '/search/view/' + [file, hash, frame].join('/')) + .replace('{metadata}', '/metadata/' + hash) + .replace('{className}', className) + .replace('{saved_msg}', isSaved ? 'Saved' : 'Save') + return t + }).join('') + } + function loadSaved() { + document.body.className = 'saving' + pushState('View saved', "/search/saved") + const saved = window.store.get('saved', []) + cropping = false + loadMessage(saved.length + " saved image" + (saved.length === 1 ? "" : "s")) + loadQuery('') + const box_el = document.querySelector('.box') + if (box_el) box_el.parentNode.removeChild(box_el) + results_el.innerHTML = saved.map(href => { + const { url, dir } = getPathFromImage({ src: href }) + let className = 'saved' + let t = result_template + .replace('{img}', href) + .replace('{browse}', '/search/q/' + dir) + .replace('{search}', '/search/fetch/?url=' + url) + .replace('{metadata}', '/metadata/' + dir) + .replace('{className}', className) + .replace('{saved_msg}', 'Saved') + return t + }).join('') + } + function loadQuery(path) { + if (cropping) return + const qd = document.querySelector('.query div') + qd.innerHTML = '' + if (path.match(/(gif|jpe?g|png)$/)) { + const img = new Image() + img.setAttribute('crossorigin', 'anonymous') + img.src = path.replace('sm', 'md') + qd.appendChild(img) + } else { + qd.innerHTML = path || "" + } + } + function loadMessage(msg) { + document.querySelector('.query .msg').innerHTML = msg + } + + // panic button + function panic() { + loadMessage('Query cleared') + loadQuery('') + results_el.innerHTML = '' + } + + // adding stuff to localstorage + function save(e) { + const { url } = getPathFromImage(e.target) + const saved = window.store.get('saved', []) + let newList = saved || [] + if (saved.indexOf(url) !== -1) { + newList = saved.filter(f => f !== url) + e.target.parentNode.classList.remove('saved') + } else { + newList.push(url) + e.target.parentNode.classList.add('saved') + } + window.store.set('saved', newList) + } + function reset() { + const shouldReset = window.confirm("This will reset the saved images. Are you sure?") + if (!shouldReset) return + window.store.set('saved', []) + loadSaved() + document.querySelector('[name=title]').value = '' + window.alert("Reset saved images") + } + + // submit the new group + function createNewGroup() { + const title = document.querySelector('[name=title]').value.trim().replace(/[^-_a-zA-Z0-9 ]/g, "") + const saved = window.store.get('saved', []) + const graphic = document.querySelector('[name=graphic]').checked + if (!title.length) return alert("Please enter a title for this group") + if (!saved.length) return alert("Please pick some images to save") + if (!did_check) { + alert('Automatically checking for duplicates. Please doublecheck your selection.') + return check() + } + if (creating) return null + creating = true + return http_post("/api/images/import/new/", { + title, + graphic, + saved + }).then(res => { + console.log(res) + window.store.set('saved', []) + window.location.href = '/groups/show/' + res.image_group.id + }).catch(res => { + alert('Error creating group. The server response is logged to the console.') + console.log(res) + creating = false + }) + } + + // api queries + function login() { + const isLocal = (window.location.hostname === '0.0.0.0') + try { + // csrftoken = "test" // getCookie('csrftoken') + const auth = JSON.parse(window.store.get('persist:root').auth) + token = auth.token + username = auth.user.username + if (!token && !isLocal) { + window.location.href = '/' + } + } catch (e) { + if (!isLocal) { + window.location.href = '/' + } + } + document.querySelector('.logged-in .capitalize').innerHTML = username || 'user' + } + + function upload(e) { + cropping = false + const files = e.dataTransfer ? e.dataTransfer.files : e.target.files + let i, f + for (i = 0, f; i < files.length; i++) { + f = files[i] + if (f && f.type.match('image.*')) break + } + if (!f) return + do_upload(f) + } + + function do_upload(f) { + const fd = new FormData() + fd.append('query_img', f) + document.body.className = 'loading' + http_post('/search/api/upload', fd).then(loadResults) + } + + function upload_again() { + const { files } = document.querySelector('input[type=file]') + if (!files.length) { + window.alert('Please upload a file.') + return + } + upload({ + dataTransfer: { files } + }) + } + + function search(e) { + if (e.length) return search_by_vector(e) + const { url } = getPath(e.target) + cropping = false + document.body.className = 'loading' + loadQuery(url) + loadMessage('Loading results...') + http_get('/search/api/fetch/?url=' + url).then(loadResults) + } + + function search_by_vector(e) { + cropping = false + document.body.className = 'loading' + loadQuery('') + loadMessage('Loading results...') + http_get('/search/api/search/' + e.join('/')).then(loadResults) + } + + function browse(e) { + document.body.className = 'loading' + cropping = false + let dir; + if (e.target.dir) { + dir = e.target.dir + } + else { + const href = e.target.parentNode.href + dir = href.split('/')[5] + console.log(href, dir) + } + loadMessage('Listing video...') + http_get('/search/api/list/' + dir).then(loadDirectory) + } + + function check() { + http_post('/api/images/import/search/', { + saved: window.store.get('saved') || [], + }).then(res => { + console.log(res) + const { good, bad } = res + did_check = true + window.store.set('saved', good) + if (!bad.length) { + return alert("No duplicates found.") + } + bad.forEach(path => { + const el = document.querySelector('img[src="' + path + '"]') + if (el) el.parentNode.classList.remove('saved') + }) + return alert("Untagged " + bad.length + " duplicate" + (bad.length === 1 ? "" : "s") + ".") + }) + } + + function random() { + http_get('/search/api/random').then(loadResults) + } + + // drawing a box + function down(e) { + e.preventDefault() + dragging = true + bounds = query_div.querySelector('img').getBoundingClientRect() + mouse_x = e.pageX + mouse_y = e.pageY + x = mouse_x - bounds.left + y = mouse_y - bounds.top + dx = dy = 0 + box = document.querySelector('.box') || document.createElement('div') + box.className = 'box' + box.style.left = x + 'px' + box.style.top = y + 'px' + box.style.width = 0 + 'px' + box.style.height = 0 + 'px' + query_div.appendChild(box) + } + function move(e) { + if (!dragging) return + e.preventDefault() + dx = clamp(e.pageX - mouse_x, 0, bounds.width - x) + dy = clamp(e.pageY - mouse_y, 0, bounds.height - y) + box.style.width = dx + 'px' + box.style.height = dy + 'px' + } + function up(e) { + if (!dragging) return + dragging = false + e.preventDefault() + const img = query_div.querySelector('img') + const canvas = query_div.querySelector('canvas') || document.createElement('canvas') + const ctx = canvas.getContext('2d') + const ratio = img.naturalWidth / bounds.width + canvas.width = dx * ratio + canvas.height = dy * ratio + if (dx < 10 || dy < 10) { + if (canvas.parentNode) canvas.parentNode.removeChild(canvas) + const box_el = document.querySelector('.box') + if (box_el) box_el.parentNode.removeChild(box_el) + return + } + query_div.appendChild(canvas) + ctx.drawImage( + img, + x * ratio, + y * ratio, + dx * ratio, + dy * ratio, + 0, 0, canvas.width, canvas.height + ) + cropping = true + const blob = window.dataUriToBlob(canvas.toDataURL('image/jpeg', 0.9)) + do_upload(blob) + } + + // utility functions + function http_get(url) { + return fetch(url).then(res => res.json()) + } + function http_post(url, data) { + let headers + if (data instanceof FormData) { + headers = { + Accept: 'application/json, application/xml, text/play, text/html, *.*', + Authorization: 'Token ' + token, + } + } else { + headers = { + Accept: 'application/json, application/xml, text/play, text/html, *.*', + 'Content-Type': 'application/json; charset=utf-8', + Authorization: 'Token ' + token, + } + data = JSON.stringify(data) + } + + // headers['X-CSRFToken'] = csrftoken + return fetch(url, { + method: 'POST', + body: data, + credentials: 'include', + headers, + }).then(res => res.json()) + } + function on(evt, sel, handler) { + document.addEventListener(evt, function (event) { + let t = event.target + while (t && t !== this) { + if (t.matches(sel)) { + handler.call(t, event) + } + t = t.parentNode + } + }) + } + function getPathFromImage(el) { + const url = el.src ? el.src : el + const partz = url.split('/') + let type, dir, fn + if (partz.length === 3) { + type = 'photo' + dir = '' + fn = '' + } + if (partz.length === 9) { + type = 'photo' + dir = partz[6] + fn = '' + } else if (partz.length === 10) { + type = 'video' + dir = partz[6] + fn = partz[7] + } + return { type, dir, fn, url } + } + function getPath(el) { + if (el.url) { + return getPathFromImage(el.url) + } if (el.dir) { + return el + } + el = el.parentNode.parentNode.parentNode.querySelector('img') + return getPathFromImage(el) + } + function pushState(txt, path) { + if (window.location.pathname === path) return + console.log('pushstate', path) + window.history.pushState({}, txt, path) + } + function preventDefault(e) { + if (e && !e.target.classList.contains('metadata')) { + e.preventDefault() + } + } + function clamp(n, a, b) { return n < a ? a : n < b ? n : b } + + // initialize the app when the DOM is ready + document.addEventListener('DOMContentLoaded', init) +} + +loadApp() diff --git a/faiss/static/js/dataUriToBlob.js b/faiss/static/js/dataUriToBlob.js new file mode 100644 index 00000000..80189b8d --- /dev/null +++ b/faiss/static/js/dataUriToBlob.js @@ -0,0 +1,58 @@ +var dataUriToUint8Array = function(uri){ + var data = uri.split(',')[1]; + var bytes = atob(data); + var buf = new ArrayBuffer(bytes.length); + var u8 = new Uint8Array(buf); + for (var i = 0; i < bytes.length; i++) { + u8[i] = bytes.charCodeAt(i); + } + return u8 +} + +window.dataUriToBlob = (function(){ +/** + * Blob constructor. + */ + +var Blob = window.Blob; + +/** + * ArrayBufferView support. + */ + +var hasArrayBufferView = new Blob([new Uint8Array(100)]).size == 100; + +/** + * Return a `Blob` for the given data `uri`. + * + * @param {String} uri + * @return {Blob} + * @api public + */ + +var dataUriToBlob = function(uri){ + var data = uri.split(',')[1]; + var bytes = atob(data); + var buf = new ArrayBuffer(bytes.length); + var arr = new Uint8Array(buf); + for (var i = 0; i < bytes.length; i++) { + arr[i] = bytes.charCodeAt(i); + } + + if (!hasArrayBufferView) arr = buf; + var blob = new Blob([arr], { type: mime(uri) }); + blob.slice = blob.slice || blob.webkitSlice; + return blob; +}; + +/** + * Return data uri mime type. + */ + +function mime(uri) { + return uri.split(';')[0].slice(5); +} + +return dataUriToBlob; + +})() diff --git a/faiss/static/js/metadata-app.js b/faiss/static/js/metadata-app.js new file mode 100644 index 00000000..fa2265fa --- /dev/null +++ b/faiss/static/js/metadata-app.js @@ -0,0 +1,50 @@ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=212)}([function(e,t,n){var r=n(99),o=36e5,i=6e4,a=2,u=/[T ]/,s=/:/,c=/^(\d{2})$/,l=[/^([+-]\d{2})$/,/^([+-]\d{3})$/,/^([+-]\d{4})$/],f=/^(\d{4})/,d=[/^([+-]\d{4})/,/^([+-]\d{5})/,/^([+-]\d{6})/],p=/^-(\d{2})$/,h=/^-?(\d{3})$/,m=/^-?(\d{2})-?(\d{2})$/,v=/^-?W(\d{2})$/,y=/^-?W(\d{2})-?(\d{1})$/,g=/^(\d{2}([.,]\d*)?)$/,_=/^(\d{2}):?(\d{2}([.,]\d*)?)$/,b=/^(\d{2}):?(\d{2}):?(\d{2}([.,]\d*)?)$/,w=/([Z+-].*)$/,x=/^(Z)$/,E=/^([+-])(\d{2})$/,O=/^([+-])(\d{2}):?(\d{2})$/;function S(e,t,n){t=t||0,n=n||0;var r=new Date(0);r.setUTCFullYear(e,0,4);var o=7*t+n+1-(r.getUTCDay()||7);return r.setUTCDate(r.getUTCDate()+o),r}e.exports=function(e,t){if(r(e))return new Date(e.getTime());if("string"!=typeof e)return new Date(e);var n=(t||{}).additionalDigits;n=null==n?a:Number(n);var T=function(e){var t,n={},r=e.split(u);if(s.test(r[0])?(n.date=null,t=r[0]):(n.date=r[0],t=r[1]),t){var o=w.exec(t);o?(n.time=t.replace(o[1],""),n.timezone=o[1]):n.time=t}return n}(e),k=function(e,t){var n,r=l[t],o=d[t];if(n=f.exec(e)||o.exec(e)){var i=n[1];return{year:parseInt(i,10),restDateString:e.slice(i.length)}}if(n=c.exec(e)||r.exec(e)){var a=n[1];return{year:100*parseInt(a,10),restDateString:e.slice(a.length)}}return{year:null}}(T.date,n),R=k.year,j=function(e,t){if(null===t)return null;var n,r,o,i;if(0===e.length)return(r=new Date(0)).setUTCFullYear(t),r;if(n=p.exec(e))return r=new Date(0),o=parseInt(n[1],10)-1,r.setUTCFullYear(t,o),r;if(n=h.exec(e)){r=new Date(0);var a=parseInt(n[1],10);return r.setUTCFullYear(t,0,a),r}if(n=m.exec(e)){r=new Date(0),o=parseInt(n[1],10)-1;var u=parseInt(n[2],10);return r.setUTCFullYear(t,o,u),r}if(n=v.exec(e))return i=parseInt(n[1],10)-1,S(t,i);if(n=y.exec(e)){i=parseInt(n[1],10)-1;var s=parseInt(n[2],10)-1;return S(t,i,s)}return null}(k.restDateString,R);if(j){var P,C=j.getTime(),M=0;return T.time&&(M=function(e){var t,n,r;if(t=g.exec(e))return(n=parseFloat(t[1].replace(",",".")))%24*o;if(t=_.exec(e))return n=parseInt(t[1],10),r=parseFloat(t[2].replace(",",".")),n%24*o+r*i;if(t=b.exec(e)){n=parseInt(t[1],10),r=parseInt(t[2],10);var a=parseFloat(t[3].replace(",","."));return n%24*o+r*i+1e3*a}return null}(T.time)),T.timezone?P=function(e){var t,n;return(t=x.exec(e))?0:(t=E.exec(e))?(n=60*parseInt(t[2],10),"+"===t[1]?-n:n):(t=O.exec(e))?(n=60*parseInt(t[2],10)+parseInt(t[3],10),"+"===t[1]?-n:n):0}(T.timezone):(P=new Date(C+M).getTimezoneOffset(),P=new Date(C+M+P*i).getTimezoneOffset()),new Date(C+M+P*i)}return new Date(e)}},function(e,t,n){"use strict";e.exports=n(213)},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(220),o=n(117),i=n(224);n.d(t,"Provider",function(){return r.b}),n.d(t,"createProvider",function(){return r.a}),n.d(t,"connectAdvanced",function(){return o.a}),n.d(t,"connect",function(){return i.a})},function(e,t){var n;n=function(){return this}();try{n=n||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";t.__esModule=!0;var r=function(e){return e&&e.__esModule?e:{default:e}}(n(340));t.default=r.default||function(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];if(u)throw u;for(var r=!1,o={},i=0;i0&&void 0!==arguments[0]?arguments[0]:0;e/=arguments.length>1&&void 0!==arguments[1]?arguments[1]:25;var t=p(Math.round(e)%60);return(e=Math.floor(e/60))>60?Math.floor(e/60)+":"+p(e%60)+":"+t:e%60+":"+t},t.percent=function(e){return(100*e).toFixed(1)+"%"},t.px=function(e,t){return Math.round(e*t)+"px"},t.clamp=function(e,t,n){return e3&&void 0!==arguments[3]?arguments[3]:"th";return["https://sa-vframe.ams3.digitaloceanspaces.com/v1/media/keyframes",d(e)?null:"unverified",h(t),f(n,6),r,"index.jpg"].filter(function(e){return!!e}).join("/")},v=(t.metadataUri=function(e,t){return"/metadata/"+e+"/"+t+"/"},t.keyframeUri=function(e,t){return"/metadata/"+e+"/keyframe/"+f(t,6)+"/"},t.preloadImage=function(e){var t=e.verified,n=e.hash,r=e.frame,o=e.url;n&&r&&(o=m(t,n,r,"md"));var i=new Image,a=!1;i.onload=function(){a||(a=!0,i.onload=null)},i.crossOrigin="anonymous",i.src=o,i.complete&&i.onload()},null),y="",g="",_=(t.post=function(e,t,n){_();var o=void 0;t instanceof FormData?o={Accept:"application/json, application/xml, text/play, text/html, *.*"}:(o={Accept:"application/json, application/xml, text/play, text/html, *.*","Content-Type":"application/json; charset=utf-8"},t=(0,r.default)(t));var i={method:"POST",body:t,headers:o,credentials:"include"};return n&&(o.Authorization="Token "+y),fetch(e,i).then(function(e){return e.json()})},t.login=function(){if(v)return v;var e="0.0.0.0"===window.location.hostname||"127.0.0.1"===window.location.hostname;try{var t=JSON.parse(JSON.parse(localStorage.getItem("persist:root")).auth);return y=t.token,g=t.user.username,y&&console.log("logged in",g),v=t,y||e||(window.location.href="/"),t}catch(t){return e||(window.location.href="/"),{}}})},function(e,t,n){var r=n(13),o=n(10),i=n(34),a=n(26),u=n(25),s=function(e,t,n){var c,l,f,d=e&s.F,p=e&s.G,h=e&s.S,m=e&s.P,v=e&s.B,y=e&s.W,g=p?o:o[t]||(o[t]={}),_=g.prototype,b=p?r:h?r[t]:(r[t]||{}).prototype;for(c in p&&(n=t),n)(l=!d&&b&&void 0!==b[c])&&u(g,c)||(f=l?b[c]:n[c],g[c]=p&&"function"!=typeof b[c]?n[c]:v&&l?i(f,r):y&&b[c]==f?function(e){var t=function(t,n,r){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,r)}return e.apply(this,arguments)};return t.prototype=e.prototype,t}(f):m&&"function"==typeof f?i(Function.call,f):f,m&&((g.virtual||(g.virtual={}))[c]=f,e&s.R&&_&&!_[c]&&a(_,c,f)))};s.F=1,s.G=2,s.S=4,s.P=8,s.B=16,s.W=32,s.U=64,s.R=128,e.exports=s},function(e,t,n){"use strict";e.exports=function(e,t,n,r,o,i,a,u){if(!e){var s;if(void 0===t)s=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,o,i,a,u],l=0;(s=new Error(t.replace(/%s/g,function(){return c[l++]}))).name="Invariant Violation"}throw s.framesToPop=1,s}}},function(e,t,n){var r=n(23);e.exports=function(e){if(!r(e))throw TypeError(e+" is not an object!");return e}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.random=t.browse=t.search=t.searchByFrame=t.searchByVerifiedFrame=t.upload=t.updateOptions=t.panic=t.publicUrl=void 0;var r=s(n(4)),o=function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}(n(39)),i=n(94),a=n(17),u=s(n(354));function s(e){return e&&e.__esModule?e:{default:e}}var c={upload:function(){return"https://syrianarchive.vframe.io/search/api/upload"},search:function(){return"https://syrianarchive.vframe.io/search/api/fetch"},searchByVerifiedFrame:function(e,t,n){return"https://syrianarchive.vframe.io/search/api/search/"+e+"/"+t+"/"+(0,a.pad)(n,6)},searchByFrame:function(e,t){return"https://syrianarchive.vframe.io/search/api/search/"+e+"/"+(0,a.pad)(t,6)},browse:function(e){return"https://syrianarchive.vframe.io/search/api/list/"+e},random:function(){return"https://syrianarchive.vframe.io/search/api/random"},check:function(){return"https://syrianarchive.vframe.io/api/images/import/search"}},l=t.publicUrl={browse:function(e){return"/search/browse/"+e},searchByVerifiedFrame:function(e,t,n){return"/search/keyframe/"+(0,a.verify)(e)+"/"+t+"/"+(0,a.pad)(n,6)},searchByFrame:function(e,t){return"/search/keyframe/"+e+"/"+(0,a.pad)(t,6)},review:function(){return"/search/review/"}},f=function(e,t){return{type:o.search.loading,tag:e,offset:t}},d=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return{type:o.search.loaded,tag:e,data:t,offset:n}},p=function(e,t){return{type:o.search.error,tag:e,err:t}};t.panic=function(){return function(e){i.history.push("/search/"),e({type:o.search.panic})}},t.updateOptions=function(e){return function(t){t({type:o.search.update_options,opt:e})}},t.upload=function(e,t){return function(n){var o=i.store.getState().search.options,u=new FormData;u.append("query_img",e),u.append("limit",o.perPage),t||n(f("query")),(0,a.post)(c.upload(),u).then(function(e){if(t){var o=e.query.timing;e.query=(0,r.default)({},t,{timing:o});var a={};if(e.query.crop){var u=e.query.crop,s=u.x,c=u.y,l=u.w,f=u.h;a.crop=[s,c,l,f].map(function(e){return parseInt(e,10)}).join(",")}t.url&&!t.hash&&(a.url=t.url)}else e.query.url&&!window.location.search.match(e.query.url)&&i.history.push("/search/?url="+e.query.url);n(d("query",e))}).catch(function(e){return n(p("query",e))})}},t.searchByVerifiedFrame=function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0;return function(o){var s=i.store.getState().search.options;o(f("query",r));var l=u.default.stringify({limit:s.perPage,offset:r});(0,a.preloadImage)({verified:e,hash:t,frame:n}),fetch(c.searchByVerifiedFrame(e,t,n)+"?"+l,{method:"GET",mode:"cors"}).then(function(e){return e.json()}).then(function(e){return o(d("query",e,r))}).catch(function(e){return o(p("query",e))})}},t.searchByFrame=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return function(r){var o=i.store.getState().search.options;r(f("query",n));var s=u.default.stringify({limit:o.perPage,offset:n});(0,a.preloadImage)({verified:!1,hash:e,frame:t}),fetch(c.searchByFrame(e,t)+"?"+s,{method:"GET",mode:"cors"}).then(function(e){return e.json()}).then(function(e){return r(d("query",e,n))}).catch(function(e){return r(p("query",e))})}},t.search=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return function(n){var r=i.store.getState().search.options;n(f("query",t));var o=u.default.stringify({url:e,limit:r.perPage,offset:t});0===e.indexOf("static")&&(0,a.preloadImage)({uri:e}),fetch(c.search(e)+"?"+o,{method:"GET",mode:"cors"}).then(function(e){return e.json()}).then(function(e){return n(d("query",e,t))}).catch(function(e){return n(p("query",e))})}},t.browse=function(e){return function(t){var n="browse";t(f(n)),fetch(c[n](e),{method:"GET",mode:"cors"}).then(function(e){return e.json()}).then(function(e){return t(d(n,e))}).catch(function(e){return t(p(n,e))})}},t.random=function(){return function(e){var t=i.store.getState().search.options,n=u.default.stringify({limit:t.perPage});e(f("query")),fetch(c.random()+"?"+n,{method:"GET",mode:"cors"}).then(function(e){return e.json()}).then(function(t){e(d("query",t)),i.history.push(l.searchByVerifiedFrame(t.query.verified,t.query.hash,t.query.frame))}).catch(function(t){return e(p("query",t))})}}},function(e,t,n){var r=n(20),o=n(125),i=n(77),a=Object.defineProperty;t.f=n(24)?Object.defineProperty:function(e,t,n){if(r(e),t=i(t,!0),r(n),o)try{return a(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(e[t]=n.value),e}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,n){e.exports=!n(35)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){var r=n(22),o=n(43);e.exports=n(24)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(275);n.d(t,"createBrowserHistory",function(){return r.a});var o=n(278);n.d(t,"createHashHistory",function(){return o.a});var i=n(279);n.d(t,"createMemoryHistory",function(){return i.a});var a=n(62);n.d(t,"createLocation",function(){return a.a}),n.d(t,"locationsAreEqual",function(){return a.b});var u=n(47);n.d(t,"parsePath",function(){return u.d}),n.d(t,"createPath",function(){return u.b})},function(e,t,n){e.exports={default:n(331),__esModule:!0}},function(e,t,n){var r=n(0),o=n(30);e.exports=function(e){var t=r(e),n=t.getFullYear(),i=new Date(0);i.setFullYear(n+1,0,4),i.setHours(0,0,0,0);var a=o(i),u=new Date(0);u.setFullYear(n,0,4),u.setHours(0,0,0,0);var s=o(u);return t.getTime()>=a.getTime()?n+1:t.getTime()>=s.getTime()?n:n-1}},function(e,t,n){var r=n(66);e.exports=function(e){return r(e,{weekStartsOn:1})}},function(e,t,n){var r=n(0);e.exports=function(e){var t=r(e);return t.setHours(0,0,0,0),t}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){"use strict";var r=n(70),o=Object.keys||function(e){var t=[];for(var n in e)t.push(n);return t};e.exports=f;var i=n(54);i.inherits=n(32);var a=n(201),u=n(111);i.inherits(f,a);for(var s=o(u.prototype),c=0;c1)for(var n=1;n=t.length?{value:void 0,done:!0}:(e=r(t,n),this._i+=e.length,{value:e,done:!1})})},function(e,t,n){var r=n(129),o=n(82);e.exports=Object.keys||function(e){return r(e,o)}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t,n){"use strict";n.d(t,"a",function(){return r}),n.d(t,"f",function(){return o}),n.d(t,"c",function(){return i}),n.d(t,"e",function(){return a}),n.d(t,"g",function(){return u}),n.d(t,"d",function(){return s}),n.d(t,"b",function(){return c});var r=function(e){return"/"===e.charAt(0)?e:"/"+e},o=function(e){return"/"===e.charAt(0)?e.substr(1):e},i=function(e,t){return new RegExp("^"+t+"(\\/|\\?|#|$)","i").test(e)},a=function(e,t){return i(e,t)?e.substr(t.length):e},u=function(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e},s=function(e){var t=e||"/",n="",r="",o=t.indexOf("#");-1!==o&&(r=t.substr(o),t=t.substr(0,o));var i=t.indexOf("?");return-1!==i&&(n=t.substr(i),t=t.substr(0,i)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}},c=function(e){var t=e.pathname,n=e.search,r=e.hash,o=t||"/";return n&&"?"!==n&&(o+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(o+="#"===r.charAt(0)?r:"#"+r),o}},function(e,t){e.exports=function(e){var t=[];return t.toString=function(){return this.map(function(t){var n=function(e,t){var n=e[1]||"",r=e[3];if(!r)return n;if(t&&"function"==typeof btoa){var o=function(e){return"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(e))))+" */"}(r),i=r.sources.map(function(e){return"/*# sourceURL="+r.sourceRoot+e+" */"});return[n].concat(i).concat([o]).join("\n")}return[n].join("\n")}(t,e);return t[2]?"@media "+t[2]+"{"+n+"}":n}).join("")},t.i=function(e,n){"string"==typeof e&&(e=[[null,e,""]]);for(var r={},o=0;o=0&&s.splice(t,1)}function h(e){var t=document.createElement("style");return void 0===e.attrs.type&&(e.attrs.type="text/css"),m(t,e.attrs),d(e,t),t}function m(e,t){Object.keys(t).forEach(function(n){e.setAttribute(n,t[n])})}function v(e,t){var n,r,o,i;if(t.transform&&e.css){if(!(i=t.transform(e.css)))return function(){};e.css=i}if(t.singleton){var s=u++;n=a||(a=h(t)),r=g.bind(null,n,s,!1),o=g.bind(null,n,s,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=function(e){var t=document.createElement("link");return void 0===e.attrs.type&&(e.attrs.type="text/css"),e.attrs.rel="stylesheet",m(t,e.attrs),d(e,t),t}(t),r=function(e,t,n){var r=n.css,o=n.sourceMap,i=void 0===t.convertToAbsoluteUrls&&o;(t.convertToAbsoluteUrls||i)&&(r=c(r));o&&(r+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(o))))+" */");var a=new Blob([r],{type:"text/css"}),u=e.href;e.href=URL.createObjectURL(a),u&&URL.revokeObjectURL(u)}.bind(null,n,t),o=function(){p(n),n.href&&URL.revokeObjectURL(n.href)}):(n=h(t),r=function(e,t){var n=t.css,r=t.media;r&&e.setAttribute("media",r);if(e.styleSheet)e.styleSheet.cssText=n;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(n))}}.bind(null,n),o=function(){p(n)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else o()}}e.exports=function(e,t){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(t=t||{}).attrs="object"==typeof t.attrs?t.attrs:{},t.singleton||"boolean"==typeof t.singleton||(t.singleton=o()),t.insertInto||(t.insertInto="head"),t.insertAt||(t.insertAt="bottom");var n=f(e,t);return l(n,t),function(e){for(var o=[],i=0;io?1:0}},function(e,t,n){(function(e){function n(e){return Object.prototype.toString.call(e)}t.isArray=function(e){return Array.isArray?Array.isArray(e):"[object Array]"===n(e)},t.isBoolean=function(e){return"boolean"==typeof e},t.isNull=function(e){return null===e},t.isNullOrUndefined=function(e){return null==e},t.isNumber=function(e){return"number"==typeof e},t.isString=function(e){return"string"==typeof e},t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=function(e){return void 0===e},t.isRegExp=function(e){return"[object RegExp]"===n(e)},t.isObject=function(e){return"object"==typeof e&&null!==e},t.isDate=function(e){return"[object Date]"===n(e)},t.isError=function(e){return"[object Error]"===n(e)||e instanceof Error},t.isFunction=function(e){return"function"==typeof e},t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=e.isBuffer}).call(t,n(204).Buffer)},function(e,t){var n=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++n+r).toString(36))}},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t,n){var r=n(22).f,o=n(25),i=n(14)("toStringTag");e.exports=function(e,t,n){e&&!o(e=n?e:e.prototype,i)&&r(e,i,{configurable:!0,value:t})}},function(e,t,n){n(254);for(var r=n(13),o=n(26),i=n(36),a=n(14)("toStringTag"),u="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),s=0;s may have only one child element"),this.unlisten=r.listen(function(){e.setState({match:e.computeMatch(r.location.pathname)})})},t.prototype.componentWillReceiveProps=function(e){o()(this.props.history===e.history,"You cannot change ")},t.prototype.componentWillUnmount=function(){this.unlisten()},t.prototype.render=function(){var e=this.props.children;return e?s.a.Children.only(e):null},t}(s.a.Component);p.propTypes={history:l.a.object.isRequired,children:l.a.node},p.contextTypes={router:l.a.object},p.childContextTypes={router:l.a.object.isRequired},t.a=p},function(e,t,n){"use strict";var r=n(140),o=n.n(r),i={},a=0;t.a=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments[2];"string"==typeof t&&(t={path:t});var r=t,u=r.path,s=r.exact,c=void 0!==s&&s,l=r.strict,f=void 0!==l&&l,d=r.sensitive,p=void 0!==d&&d;if(null==u)return n;var h=function(e,t){var n=""+t.end+t.strict+t.sensitive,r=i[n]||(i[n]={});if(r[e])return r[e];var u=[],s={re:o()(e,u,t),keys:u};return a<1e4&&(r[e]=s,a++),s}(u,{end:c,strict:f,sensitive:p}),m=h.re,v=h.keys,y=m.exec(e);if(!y)return null;var g=y[0],_=y.slice(1),b=e===g;return c&&!b?null:{path:u,url:"/"===u&&""===g?"/":g,isExact:b,params:v.reduce(function(e,t,n){return e[t.name]=_[n],e},{})}}},function(e,t,n){"use strict";t.__esModule=!0;var r=function(e){return e&&e.__esModule?e:{default:e}}(n(126));t.default=function(e,t,n){return t in e?(0,r.default)(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}},function(e,t,n){var r=n(0);e.exports=function(e,t){var n=t&&Number(t.weekStartsOn)||0,o=r(e),i=o.getDay(),a=(i0?r:n)(e)}},function(e,t,n){var r=n(20),o=n(251),i=n(82),a=n(74)("IE_PROTO"),u=function(){},s=function(){var e,t=n(76)("iframe"),r=i.length;for(t.style.display="none",n(131).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write(" + diff --git a/faiss/static/search.html b/faiss/static/search.html new file mode 100644 index 00000000..056d06c1 --- /dev/null +++ b/faiss/static/search.html @@ -0,0 +1 @@ +search.html \ No newline at end of file diff --git a/faiss/util.py b/faiss/util.py new file mode 100644 index 00000000..97afbc22 --- /dev/null +++ b/faiss/util.py @@ -0,0 +1,29 @@ +import time +import simplejson as json +import pickle +from os import path +from collections import namedtuple + +# Converts JSON el['key'] to Pythonic object-style el.key +def _json_object_hook(d): + return namedtuple('X', d.keys())(*d.values()) + +# Load a JSON recipe +def load_recipe(path): + with open(path) as fh: + return json.load(fh, object_hook=_json_object_hook) + +# Load a pickle file +def load_pickle(data_dir, pkl_fn): + load_start = time.time() + with open(path.join(str(data_dir), str(pkl_fn)), 'rb') as fh: + raw = fh.read() + data = pickle.loads(raw) + load_end = time.time() + load_time = load_end - load_start + print("Pickle load time: {:.1f}s".format(load_time)) + return data + +def read_json(fn): + with open(fn, 'r') as json_file: + return json.load(json_file) diff --git a/faiss/wsgi.py b/faiss/wsgi.py new file mode 100644 index 00000000..371862fb --- /dev/null +++ b/faiss/wsgi.py @@ -0,0 +1,5 @@ +from server import app + +if __name__ == "__main__": + app.run() + diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index ecca0c7f..525492f1 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -14,7 +14,6 @@ connection_url = "mysql+mysqldb://{}:{}@{}/{}".format( # Session = sessionmaker(bind=engine) # session = Session() - class SqlDataset: """ Bridge between the facial information CSVs connected to the datasets, and MySQL diff --git a/megapixels/app/server/api/image.py b/megapixels/app/server/api/image.py new file mode 100644 index 00000000..f2f4a4f9 --- /dev/null +++ b/megapixels/app/server/api/image.py @@ -0,0 +1,40 @@ +from flask import Blueprint, render_template, abort +# from jinja2 import TemplateNotFound + +router = Blueprint('image', __name__) + +@router.route('//test', methods=['POST']) +def test(name): + # dataset = +@router.route('//face', methods=['POST']) +def upload(name): + file = request.files['query_img'] + fn = file.filename + if fn.endswith('blob'): + fn = 'filename.jpg' + + basename, ext = os.path.splitext(fn) + print("got {}, type {}".format(basename, ext)) + if ext.lower() not in valid_exts: + return jsonify({ 'error': 'not an image' }) + + uploaded_fn = datetime.now().isoformat() + "_" + basename + uploaded_fn = sanitize_re.sub('', uploaded_fn) + uploaded_img_path = "static/uploaded/" + uploaded_fn + ext + uploaded_img_path = uploaded_img_path.lower() + print('query: {}'.format(uploaded_img_path)) + + img = Image.open(file.stream).convert('RGB') + # img.save(uploaded_img_path) + # vec = db.load_feature_vector_from_file(uploaded_img_path) + vec = fe.extract(img) + # print(vec.shape) + + results = db.search(vec, limit=limit) + query = { + 'timing': time.time() - start, + } + print(results) + return jsonify({ + 'results': results, + }) diff --git a/megapixels/app/server/create.py b/megapixels/app/server/create.py new file mode 100644 index 00000000..1119ee8f --- /dev/null +++ b/megapixels/app/server/create.py @@ -0,0 +1,27 @@ +from flask import Flask, Blueprint +from flask_sqlalchemy import SQLAlchemy +from app.models.sql_factory import connection_url + +from app.server.api import router as api_router + +# from app.server.views.assets import assets + +db = SQLAlchemy() + +def create_app(script_info=None): + app = Flask(__name__, static_url_path='') + app.config['SQLALCHEMY_DATABASE_URI'] = connection_url + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + db.init_app(app) + app.register_blueprint(api) + + @app.route('/', methods=['GET']) + def index(): + return app.send_static_file('index.html') + + @app.shell_context_processor + def shell_context(): + return { 'app': app, 'db': db } + + return app diff --git a/megapixels/app/server/static b/megapixels/app/server/static new file mode 120000 index 00000000..1dc7a639 --- /dev/null +++ b/megapixels/app/server/static @@ -0,0 +1 @@ +../../../site/public \ No newline at end of file diff --git a/megapixels/cli_flask.py b/megapixels/cli_flask.py new file mode 100644 index 00000000..369bec01 --- /dev/null +++ b/megapixels/cli_flask.py @@ -0,0 +1,19 @@ +# -------------------------------------------------------- +# wrapper for flask CLI API +# -------------------------------------------------------- + +import click + +from flask.cli import FlaskGroup +from app.server.create import create_app + +# from app.settings import app_cfg as cfg +# from app.utils import logger_utils + +cli = FlaskGroup(create_app=create_app) + +# -------------------------------------------------------- +# Entrypoint +# -------------------------------------------------------- +if __name__ == '__main__': + cli() diff --git a/megapixels/commands/faiss/build_db.py b/megapixels/commands/faiss/build_db.py index c90d178b..52c4980f 100644 --- a/megapixels/commands/faiss/build_db.py +++ b/megapixels/commands/faiss/build_db.py @@ -17,11 +17,15 @@ from app.settings import app_cfg as cfg def cli(ctx): """import the various CSVs into MySQL """ - datasets = [] + load_sql_datasets(clobber=True) + +def load_sql_datasets(path, clobber=False): + datasets = {} for path in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): - build_dataset(path) + dataset = load_sql_dataset(path, clobber) + datasets[dataset.name] = dataset -def build_dataset(path): +def load_sql_dataset(path, clobber=False): name = os.path.basename(path) dataset = SqlDataset(name) @@ -30,9 +34,12 @@ def build_dataset(path): table = dataset.get_table(key) if table is None: continue - df = pd.read_csv(fn) + if clobber: + df = pd.read_csv(fn) + + # fix columns that are named "index", a sql reserved word + df.columns = table.__table__.columns.keys() - # fix columns that are named "index", a sql reserved word - df.columns = table.__table__.columns.keys() + df.to_sql(name=table.__tablename__, con=engine, if_exists='replace', index=False) - df.to_sql(name=table.__tablename__, con=engine, if_exists='replace', index=False) + return dataset \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 38746f284b17400d4e2555509ea60df5912b824a Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Fri, 14 Dec 2018 18:10:27 +0100 Subject: all the sql stuff communicating nicely --- megapixels/app/models/sql_factory.py | 61 ++++++++++++++++++++++++++--- megapixels/app/server/api.py | 72 +++++++++++++++++++++++++++++++++++ megapixels/app/server/api/image.py | 40 ------------------- megapixels/app/server/create.py | 23 +++++++---- megapixels/commands/faiss/build_db.py | 36 ++---------------- 5 files changed, 147 insertions(+), 85 deletions(-) create mode 100644 megapixels/app/server/api.py delete mode 100644 megapixels/app/server/api/image.py (limited to 'megapixels/app/models/sql_factory.py') diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 525492f1..2a18d6af 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -1,9 +1,15 @@ import os +import glob +import time +import pandas as pd from sqlalchemy import create_engine, Table, Column, String, Integer, DateTime, Float from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base +from app.utils.file_utils import load_recipe, load_csv_safe +from app.settings import app_cfg as cfg + connection_url = "mysql+mysqldb://{}:{}@{}/{}".format( os.getenv("DB_USER"), os.getenv("DB_PASS"), @@ -11,8 +17,49 @@ connection_url = "mysql+mysqldb://{}:{}@{}/{}".format( os.getenv("DB_NAME") ) -# Session = sessionmaker(bind=engine) -# session = Session() +datasets = {} +loaded = False + +def list_datasets(): + return [{ + 'name': name, + 'tables': list(datasets[name].tables.keys()), + } for name in datasets.keys()] + +def get_dataset(name): + return datasets[name] if name in datasets else None + +def get_table(name, table_name): + dataset = get_dataset(name) + return dataset.get_table(table_name) if dataset else None + +def load_sql_datasets(replace=False, base_model=None): + global datasets, loaded + if loaded: + return datasets + engine = create_engine(connection_url) if replace else None + for path in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): + dataset = load_sql_dataset(path, replace, engine, base_model) + datasets[dataset.name] = dataset + loaded = True + return datasets + +def load_sql_dataset(path, replace=False, engine=None, base_model=None): + name = os.path.basename(path) + dataset = SqlDataset(name, base_model=base_model) + + for fn in glob.iglob(os.path.join(path, "*.csv")): + key = os.path.basename(fn).replace(".csv", "") + table = dataset.get_table(key) + if table is None: + continue + if replace: + print('loading dataset {}'.format(fn)) + df = pd.read_csv(fn) + # fix columns that are named "index", a sql reserved word + df.columns = table.__table__.columns.keys() + df.to_sql(name=table.__tablename__, con=engine, if_exists='replace', index=False) + return dataset class SqlDataset: """ @@ -21,18 +68,18 @@ class SqlDataset: - names will be fixed to work in SQL (index -> id) - we can then have more generic models for fetching this info after doing a FAISS query """ - def __init__(self, name, base_model=None): + def __init__(self, name, engine=None, base_model=None): self.name = name self.tables = {} if base_model is None: - engine = create_engine(connection_url) + self.engine = create_engine(connection_url) base_model = declarative_base(engine) self.base_model = base_model def get_table(self, type): if type in self.tables: return self.tables[type] - elif type == 'uuid': + elif type == 'uuids': self.tables[type] = self.uuid_table() elif type == 'roi': self.tables[type] = self.roi_table() @@ -96,3 +143,7 @@ class SqlDataset: roll = Column(Float, nullable=False) yaw = Column(Float, nullable=False) return Pose + + +# Session = sessionmaker(bind=engine) +# session = Session() diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py new file mode 100644 index 00000000..e7db11f1 --- /dev/null +++ b/megapixels/app/server/api.py @@ -0,0 +1,72 @@ +from flask import Blueprint, jsonify + +from app.models.sql_factory import list_datasets, get_dataset, get_table + +# from jinja2 import TemplateNotFound + +# import os +# import sys +# import json +# import time +# import argparse +# import cv2 as cv +# import numpy as np +# from datetime import datetime +# from flask import Flask, request, render_template, jsonify +# from PIL import Image # todo: try to remove PIL dependency +# import re + +# sanitize_re = re.compile('[\W]+') +# valid_exts = ['.gif', '.jpg', '.jpeg', '.png'] + +# from dotenv import load_dotenv +# load_dotenv() + +# from feature_extractor import FeatureExtractor + +# DEFAULT_LIMIT = 50 + +api = Blueprint('api', __name__) + +@api.route('/') +def index(): + return jsonify({ 'datasets': list_datasets() }) + +@api.route('/dataset//test', methods=['POST']) +def test(dataset='test'): + dataset = get_dataset(dataset) + print('hiiiiii') + return jsonify({ 'test': 'OK', 'dataset': dataset }) + +# @router.route('//face', methods=['POST']) +# def upload(name): +# file = request.files['query_img'] +# fn = file.filename +# if fn.endswith('blob'): +# fn = 'filename.jpg' + +# basename, ext = os.path.splitext(fn) +# print("got {}, type {}".format(basename, ext)) +# if ext.lower() not in valid_exts: +# return jsonify({ 'error': 'not an image' }) + +# uploaded_fn = datetime.now().isoformat() + "_" + basename +# uploaded_fn = sanitize_re.sub('', uploaded_fn) +# uploaded_img_path = "static/uploaded/" + uploaded_fn + ext +# uploaded_img_path = uploaded_img_path.lower() +# print('query: {}'.format(uploaded_img_path)) + +# img = Image.open(file.stream).convert('RGB') +# # img.save(uploaded_img_path) +# # vec = db.load_feature_vector_from_file(uploaded_img_path) +# vec = fe.extract(img) +# # print(vec.shape) + +# results = db.search(vec, limit=limit) +# query = { +# 'timing': time.time() - start, +# } +# print(results) +# return jsonify({ +# 'results': results, +# }) diff --git a/megapixels/app/server/api/image.py b/megapixels/app/server/api/image.py deleted file mode 100644 index f2f4a4f9..00000000 --- a/megapixels/app/server/api/image.py +++ /dev/null @@ -1,40 +0,0 @@ -from flask import Blueprint, render_template, abort -# from jinja2 import TemplateNotFound - -router = Blueprint('image', __name__) - -@router.route('//test', methods=['POST']) -def test(name): - # dataset = -@router.route('//face', methods=['POST']) -def upload(name): - file = request.files['query_img'] - fn = file.filename - if fn.endswith('blob'): - fn = 'filename.jpg' - - basename, ext = os.path.splitext(fn) - print("got {}, type {}".format(basename, ext)) - if ext.lower() not in valid_exts: - return jsonify({ 'error': 'not an image' }) - - uploaded_fn = datetime.now().isoformat() + "_" + basename - uploaded_fn = sanitize_re.sub('', uploaded_fn) - uploaded_img_path = "static/uploaded/" + uploaded_fn + ext - uploaded_img_path = uploaded_img_path.lower() - print('query: {}'.format(uploaded_img_path)) - - img = Image.open(file.stream).convert('RGB') - # img.save(uploaded_img_path) - # vec = db.load_feature_vector_from_file(uploaded_img_path) - vec = fe.extract(img) - # print(vec.shape) - - results = db.search(vec, limit=limit) - query = { - 'timing': time.time() - start, - } - print(results) - return jsonify({ - 'results': results, - }) diff --git a/megapixels/app/server/create.py b/megapixels/app/server/create.py index 1119ee8f..9efed669 100644 --- a/megapixels/app/server/create.py +++ b/megapixels/app/server/create.py @@ -1,10 +1,8 @@ -from flask import Flask, Blueprint +from flask import Flask, Blueprint, jsonify from flask_sqlalchemy import SQLAlchemy -from app.models.sql_factory import connection_url +from app.models.sql_factory import connection_url, load_sql_datasets -from app.server.api import router as api_router - -# from app.server.views.assets import assets +from app.server.api import api db = SQLAlchemy() @@ -14,8 +12,10 @@ def create_app(script_info=None): app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) - app.register_blueprint(api) - + datasets = load_sql_datasets(replace=False, base_model=db.Model) + + app.register_blueprint(api, url_prefix='/api') + @app.route('/', methods=['GET']) def index(): return app.send_static_file('index.html') @@ -24,4 +24,13 @@ def create_app(script_info=None): def shell_context(): return { 'app': app, 'db': db } + @app.route("/site-map") + def site_map(): + links = [] + for rule in app.url_map.iter_rules(): + # url = url_for(rule.endpoint, **(rule.defaults or {})) + # print(url) + links.append((rule.endpoint)) + return(jsonify(links)) + return app diff --git a/megapixels/commands/faiss/build_db.py b/megapixels/commands/faiss/build_db.py index 52c4980f..0f979e41 100644 --- a/megapixels/commands/faiss/build_db.py +++ b/megapixels/commands/faiss/build_db.py @@ -2,44 +2,14 @@ Load all the CSV files into MySQL """ -import os -import glob import click -import time -import pandas as pd -from app.models.sql_factory import engine, SqlDataset -from app.utils.file_utils import load_recipe, load_csv_safe -from app.settings import app_cfg as cfg +from app.models.sql_factory import load_sql_datasets @click.command() @click.pass_context def cli(ctx): """import the various CSVs into MySQL """ - load_sql_datasets(clobber=True) - -def load_sql_datasets(path, clobber=False): - datasets = {} - for path in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): - dataset = load_sql_dataset(path, clobber) - datasets[dataset.name] = dataset - -def load_sql_dataset(path, clobber=False): - name = os.path.basename(path) - dataset = SqlDataset(name) - - for fn in glob.iglob(os.path.join(path, "*.csv")): - key = os.path.basename(fn).replace(".csv", "") - table = dataset.get_table(key) - if table is None: - continue - if clobber: - df = pd.read_csv(fn) - - # fix columns that are named "index", a sql reserved word - df.columns = table.__table__.columns.keys() - - df.to_sql(name=table.__tablename__, con=engine, if_exists='replace', index=False) - - return dataset \ No newline at end of file + print('Loading CSV datasets into SQL...') + load_sql_datasets(replace=True) -- cgit v1.2.3-70-g09d2 From 2ee8cd6a77c3efed77e58d706f4ee76418770e54 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Fri, 14 Dec 2018 18:17:15 +0100 Subject: sub apis workin --- megapixels/app/models/sql_factory.py | 11 +++++++---- megapixels/app/server/api.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) (limited to 'megapixels/app/models/sql_factory.py') diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 2a18d6af..e35c3e15 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -21,10 +21,7 @@ datasets = {} loaded = False def list_datasets(): - return [{ - 'name': name, - 'tables': list(datasets[name].tables.keys()), - } for name in datasets.keys()] + return [dataset.describe() for dataset in datasets.values()] def get_dataset(name): return datasets[name] if name in datasets else None @@ -76,6 +73,12 @@ class SqlDataset: base_model = declarative_base(engine) self.base_model = base_model + def describe(self): + return { + 'name': self.name, + 'tables': list(self.tables.keys()), + } + def get_table(self, type): if type in self.tables: return self.tables[type] diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index e7db11f1..428c53b1 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -32,11 +32,18 @@ api = Blueprint('api', __name__) def index(): return jsonify({ 'datasets': list_datasets() }) -@api.route('/dataset//test', methods=['POST']) -def test(dataset='test'): - dataset = get_dataset(dataset) +@api.route('/dataset/') +def show(name): + dataset = get_dataset(name) + if dataset: + return jsonify(dataset.describe()) + else: + return jsonify({ 'status': 404 }) + +@api.route('/dataset//test', methods=['POST']) +def test(name): print('hiiiiii') - return jsonify({ 'test': 'OK', 'dataset': dataset }) + return jsonify({ 'test': 'OK', 'dataset': name }) # @router.route('//face', methods=['POST']) # def upload(name): -- cgit v1.2.3-70-g09d2 From 4cf8581655c34698f8869bb364b6d436b881d17a Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 00:35:19 +0100 Subject: returning results...! --- client/faceSearch/faceSearch.query.js | 26 +++++++------ megapixels/app/models/sql_factory.py | 64 ++++++++++++++++++++++++++++--- megapixels/app/processors/faiss.py | 58 ++++++++++++++++++++++++++++ megapixels/app/server/api.py | 53 ++++++++++++++++++++----- megapixels/app/server/json_encoder.py | 17 ++++++++ megapixels/commands/faiss/build_faiss.py | 36 +---------------- site/assets/img/ajax-loader.gif | Bin 1849 -> 0 bytes site/assets/img/loader.gif | Bin 0 -> 1849 bytes 8 files changed, 193 insertions(+), 61 deletions(-) create mode 100644 megapixels/app/processors/faiss.py create mode 100644 megapixels/app/server/json_encoder.py delete mode 100644 site/assets/img/ajax-loader.gif create mode 100644 site/assets/img/loader.gif (limited to 'megapixels/app/models/sql_factory.py') diff --git a/client/faceSearch/faceSearch.query.js b/client/faceSearch/faceSearch.query.js index 8302e437..425cb282 100644 --- a/client/faceSearch/faceSearch.query.js +++ b/client/faceSearch/faceSearch.query.js @@ -20,12 +20,12 @@ class FaceSearchQuery extends Component { if (file && file.type.match('image.*')) break } if (!file) return - var fr = new FileReader(); + const fr = new FileReader() fr.onload = () => { fr.onload = null this.setState({ image: fr.result }) } - fr.readAsDataURL(files[0]); + fr.readAsDataURL(files[0]) this.props.actions.upload(this.props.payload, file) } @@ -36,6 +36,7 @@ class FaceSearchQuery extends Component { if (image) { style.backgroundImage = 'url(' + image + ')' style.backgroundSize = 'cover' + style.opacity = 1 } return (
@@ -44,9 +45,8 @@ class FaceSearchQuery extends Component {
- : -
- + :
+ {image ? null : } }
-
+

Search This Dataset

Searching {13456} images

- Use facial recognition to reverse search into the LFW dataset and see if it contains your photos. + {'Use facial recognition to reverse search into the LFW dataset '} + {'and see if it contains your photos.'}

    -
  1. Upload a photo of yourself
  2. -
  3. Use a photo similar to examples below
  4. -
  5. Only matches over 85% will be displayed
  6. -
  7. Read more tips to improve search results
  8. -
  9. Your search data is never stored and immediately cleared once you leave this page.
  10. +
  11. Upload a photo of yourself
  12. +
  13. Use a photo similar to examples below
  14. +
  15. Only matches over 85% will be displayed
  16. +
  17. Read more tips to improve search results
  18. +
  19. {'Your search data is never stored and immediately cleared '} + {'once you leave this page.'}

Read more about privacy. diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index e35c3e15..0f7e73a0 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -19,6 +19,7 @@ connection_url = "mysql+mysqldb://{}:{}@{}/{}".format( datasets = {} loaded = False +Session = None def list_datasets(): return [dataset.describe() for dataset in datasets.values()] @@ -31,10 +32,11 @@ def get_table(name, table_name): return dataset.get_table(table_name) if dataset else None def load_sql_datasets(replace=False, base_model=None): - global datasets, loaded + global datasets, loaded, Session if loaded: return datasets - engine = create_engine(connection_url) if replace else None + engine = create_engine(connection_url) + Session = sessionmaker(bind=engine) for path in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): dataset = load_sql_dataset(path, replace, engine, base_model) datasets[dataset.name] = dataset @@ -79,6 +81,27 @@ class SqlDataset: 'tables': list(self.tables.keys()), } + def get_identity(self, id): + table = self.get_table('identity_meta') + identity = table.query.filter(table.image_id >= id).order_by(table.image_id.asc()).first().toJSON() + print(identity) + return { + 'uuid': self.select('uuids', id), + 'identity': identity, + 'roi': self.select('roi', id), + 'pose': self.select('pose', id), + } + + def select(self, table, id): + table = self.get_table(table) + if not table: + return None + session = Session() + # for obj in session.query(table).filter_by(id=id): + print(table) + obj = session.query(table).filter(table.id == id).first() + return obj.toJSON() + def get_table(self, type): if type in self.tables: return self.tables[type] @@ -102,6 +125,11 @@ class SqlDataset: __tablename__ = self.name + "_uuid" id = Column(Integer, primary_key=True) uuid = Column(String(36), nullable=False) + def toJSON(self): + return { + 'id': self.id, + 'uuid': self.uuid, + } return UUID # ==> roi.csv <== @@ -118,6 +146,17 @@ class SqlDataset: w = Column(Float, nullable=False) x = Column(Float, nullable=False) y = Column(Float, nullable=False) + def toJSON(self): + return { + 'id': self.id, + 'image_index': self.image_index, + 'image_height': self.image_height, + 'image_width': self.image_width, + 'w': self.w, + 'h': self.h, + 'x': self.x, + 'y': self.y, + } return ROI # ==> identity.csv <== @@ -132,6 +171,15 @@ class SqlDataset: gender = Column(String(1), 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 # ==> pose.csv <== @@ -145,8 +193,12 @@ class SqlDataset: 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, + 'pitch': self.pitch, + 'roll': self.roll, + 'yaw': self.yaw, + } return Pose - - -# Session = sessionmaker(bind=engine) -# session = Session() diff --git a/megapixels/app/processors/faiss.py b/megapixels/app/processors/faiss.py new file mode 100644 index 00000000..5156ad71 --- /dev/null +++ b/megapixels/app/processors/faiss.py @@ -0,0 +1,58 @@ +""" +Index all of the FAISS datasets +""" + +import os +import glob +import faiss +import time +import numpy as np + +from app.utils.file_utils import load_recipe, load_csv_safe +from app.settings import app_cfg as cfg + +class DefaultRecipe: + def __init__(self): + self.dim = 128 + self.factory_type = 'Flat' + +def build_all_faiss_databases(): + datasets = [] + for fn in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): + name = os.path.basename(fn) + recipe_fn = os.path.join(cfg.DIR_FAISS_RECIPES, name + ".json") + if os.path.exists(recipe_fn): + build_faiss_database(name, load_recipe(recipe_fn)) + else: + build_faiss_database(name, DefaultRecipe()) + +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") + + index = faiss.index_factory(recipe.dim, recipe.factory_type) + + keys, rows = load_csv_safe(vec_fn) + feats = np.array([ list(map(float, row[3].split(","))) for row in rows ]).astype('float32') + n, d = feats.shape + + print("{}: training {} x {} dim vectors".format(name, n, d)) + print(recipe.factory_type) + + add_start = time.time() + index.add(feats) + add_end = time.time() + add_time = add_end - add_start + print("{}: add time: {:.1f}s".format(name, add_time)) + + faiss.write_index(index, index_fn) + +def load_faiss_databases(): + faiss_datasets = {} + for fn in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): + name = os.path.basename(fn) + index_fn = os.path.join(cfg.DIR_FAISS_INDEXES, name + ".index") + if os.path.exists(index_fn): + index = faiss.read_index(index_fn) + faiss_datasets[name] = index + return faiss_datasets diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index cf8241bd..36563910 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -2,18 +2,23 @@ import os import re import time import dlib +import numpy as np from flask import Blueprint, request, jsonify from PIL import Image # todo: try to remove PIL dependency from app.processors import face_recognition from app.processors import face_detector -from app.models.sql_factory import list_datasets, get_dataset, get_table +from app.processors.faiss import load_faiss_databases +from app.models.sql_factory import load_sql_datasets, list_datasets, get_dataset, get_table +from app.utils.im_utils import pil2np sanitize_re = re.compile('[\W]+') valid_exts = ['.gif', '.jpg', '.jpeg', '.png'] api = Blueprint('api', __name__) +faiss_datasets = load_faiss_databases() + @api.route('/') def index(): return jsonify({ 'datasets': list_datasets() }) @@ -26,10 +31,15 @@ def show(name): else: return jsonify({ 'status': 404 }) -@api.route('/dataset//face', methods=['POST']) +@api.route('/dataset//face/', methods=['POST']) def upload(name): start = time.time() dataset = get_dataset(name) + if name not in faiss_datasets: + return jsonify({ + 'error': 'invalid dataset' + }) + faiss_dataset = faiss_datasets[name] file = request.files['query_img'] fn = file.filename if fn.endswith('blob'): @@ -40,22 +50,46 @@ def upload(name): if ext.lower() not in valid_exts: return jsonify({ 'error': 'not an image' }) - img = Image.open(file.stream).convert('RGB') + im = Image.open(file.stream).convert('RGB') + im_np = pil2np(im) # Face detection detector = face_detector.DetectorDLIBHOG() # get detection as BBox object - bboxes = detector.detect(im, largest=True) + bboxes = detector.detect(im_np, largest=True) bbox = bboxes[0] - dim = im.shape[:2][::-1] + dim = im_np.shape[:2][::-1] bbox = bbox.to_dim(dim) # convert back to real dimensions # face recognition/vector recognition = face_recognition.RecognitionDLIB(gpu=-1) + vec = recognition.vec(im_np, bbox) + + # print(vec) + query = np.array([ vec ]).astype('float32') + + # query FAISS! + distances, indexes = faiss_dataset.search(query, 5) + + if len(indexes) == 0: + print("weird, no results!") + return [] + + # get the results for this single query... + distances = distances[0] + indexes = indexes[0] - # print(vec.shape) - # results = db.search(vec, limit=limit) + if len(indexes) == 0: + print("no results!") + return [] + + lookup = {} + for _d, _i in zip(distances, indexes): + lookup[_i+1] = _d + + print(distances) + print(indexes) # with the result we have an ID # query the sql dataset for the UUID etc here @@ -63,12 +97,13 @@ def upload(name): query = { 'timing': time.time() - start, } - results = [] + results = [ dataset.get_identity(index) for index in indexes ] print(results) return jsonify({ - 'query': query, 'results': results, + # 'distances': distances.tolist(), + # 'indexes': indexes.tolist(), }) @api.route('/dataset//name', methods=['GET']) diff --git a/megapixels/app/server/json_encoder.py b/megapixels/app/server/json_encoder.py new file mode 100644 index 00000000..89af578a --- /dev/null +++ b/megapixels/app/server/json_encoder.py @@ -0,0 +1,17 @@ +from sqlalchemy.ext.declarative import DeclarativeMeta +from flask import json + +class AlchemyEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o.__class__, DeclarativeMeta): + data = {} + fields = o.__json__() if hasattr(o, '__json__') else dir(o) + for field in [f for f in fields if not f.startswith('_') and f not in ['metadata', 'query', 'query_class']]: + value = o.__getattribute__(field) + try: + json.dumps(value) + data[field] = value + except TypeError: + data[field] = None + return data + return json.JSONEncoder.default(self, o) diff --git a/megapixels/commands/faiss/build_faiss.py b/megapixels/commands/faiss/build_faiss.py index ec94c924..fc6b37ce 100644 --- a/megapixels/commands/faiss/build_faiss.py +++ b/megapixels/commands/faiss/build_faiss.py @@ -11,11 +11,7 @@ import numpy as np from app.utils.file_utils import load_recipe, load_csv_safe from app.settings import app_cfg as cfg - -class DefaultRecipe: - def __init__(self): - self.dim = 128 - self.factory_type = 'Flat' +from app.processors.faiss import build_all_faiss_databases @click.command() @click.pass_context @@ -25,32 +21,4 @@ def cli(ctx): - uses the recipe above by default - however you can override this by adding a new recipe in faiss/recipes/{name}.json """ - datasets = [] - for fn in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): - name = os.path.basename(fn) - recipe_fn = os.path.join(cfg.DIR_FAISS_RECIPES, name + ".json") - if os.path.exists(recipe_fn): - build_faiss(name, load_recipe(recipe_fn)) - else: - build_faiss(name, DefaultRecipe()) - -def build_faiss(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") - - index = faiss.index_factory(recipe.dim, recipe.factory_type) - - keys, rows = load_csv_safe(vec_fn) - feats = np.array([ list(map(float, row[3].split(","))) for row in rows ]).astype('float32') - n, d = feats.shape - - print("{}: training {} x {} dim vectors".format(name, n, d)) - print(recipe.factory_type) - - add_start = time.time() - index.add(feats) - add_end = time.time() - add_time = add_end - add_start - print("{}: add time: {:.1f}s".format(name, add_time)) - - faiss.write_index(index, index_fn) + build_all_faiss_databases() diff --git a/site/assets/img/ajax-loader.gif b/site/assets/img/ajax-loader.gif deleted file mode 100644 index dc21df18..00000000 Binary files a/site/assets/img/ajax-loader.gif and /dev/null differ diff --git a/site/assets/img/loader.gif b/site/assets/img/loader.gif new file mode 100644 index 00000000..dc21df18 Binary files /dev/null and b/site/assets/img/loader.gif differ -- cgit v1.2.3-70-g09d2 From d7df4ee5b9e24a9cdf2bf4d1bc2e73e97352afdc Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 01:02:40 +0100 Subject: searches execute --- client/actions.js | 6 +----- client/faceSearch/faceSearch.result.js | 35 ++++++++++++++++++++++++++++++---- megapixels/app/models/sql_factory.py | 5 +++-- megapixels/app/server/api.py | 9 +++++---- site/assets/css/applets.css | 17 +++++++++++++++++ 5 files changed, 57 insertions(+), 15 deletions(-) (limited to 'megapixels/app/models/sql_factory.py') diff --git a/client/actions.js b/client/actions.js index 37b4eb2e..bb011838 100644 --- a/client/actions.js +++ b/client/actions.js @@ -1,9 +1,5 @@ import * as faceSearch from './faceSearch/faceSearch.actions' -// import * as review from './review/review.actions' -// import * as metadata from './metadata/metadata.actions' export { - // search, - // review, - // metadata, + faceSearch } diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index 844a5a70..2b223a46 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -1,17 +1,44 @@ import React, { Component } from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' +import { courtesyS } from '../util' import * as actions from './faceSearch.actions' class FaceSearchResult extends Component { - componentDidMount() { - } - render() { + const { dataset } = this.props.payload + const { distances, results } = this.props.result + if (!results) { + return ( +

+ ) + } + if (!this.props.result.results.length) { + return ( +
No results
+ ) + } + const els = results.map((result, i) => { + const distance = distances[i] + const { uuid } = result.uuid + const { fullname, gender, description, images } = result.identity + return ( +
+ + {fullname} {'('}{gender}{')'}
+ {description}
+ {courtesyS(images, 'image')}
+ {distance} +
+ ) + }) + return (
- Result here +
+ {els} +
) } diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 0f7e73a0..9a44941b 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -83,8 +83,8 @@ class SqlDataset: def get_identity(self, id): table = self.get_table('identity_meta') - identity = table.query.filter(table.image_id >= id).order_by(table.image_id.asc()).first().toJSON() - print(identity) + # id += 1 + identity = table.query.filter(table.image_id <= id).order_by(table.image_id.desc()).first().toJSON() return { 'uuid': self.select('uuids', id), 'identity': identity, @@ -100,6 +100,7 @@ class SqlDataset: # for obj in session.query(table).filter_by(id=id): print(table) obj = session.query(table).filter(table.id == id).first() + session.close() return obj.toJSON() def get_table(self, type): diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 36563910..2f78ecd3 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -70,7 +70,7 @@ def upload(name): query = np.array([ vec ]).astype('float32') # query FAISS! - distances, indexes = faiss_dataset.search(query, 5) + distances, indexes = faiss_dataset.search(query, 10) if len(indexes) == 0: print("weird, no results!") @@ -85,6 +85,7 @@ def upload(name): return [] lookup = {} + ids = [i+1 for i in indexes] for _d, _i in zip(distances, indexes): lookup[_i+1] = _d @@ -97,13 +98,13 @@ def upload(name): query = { 'timing': time.time() - start, } - results = [ dataset.get_identity(index) for index in indexes ] + results = [ dataset.get_identity(id) for id in ids ] print(results) return jsonify({ 'results': results, - # 'distances': distances.tolist(), - # 'indexes': indexes.tolist(), + 'distances': distances.tolist(), + 'indexes': indexes.tolist(), }) @api.route('/dataset//name', methods=['GET']) diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index 54508f44..a01703d5 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -17,6 +17,23 @@ justify-content: flex-start; } +.results { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} +.results > div { + width: 200px; + margin-left: 20px; + margin-bottom: 40px; + font-size: 8pt; +} +.results > div img { + margin-bottom: 4px; +} +.results > div:nth-child(3n+1) { + margin-left: 0; +} .query h2 { margin-top: 0; padding-top: 0; } -- cgit v1.2.3-70-g09d2 From 1c82f7ec6a603978322e16470547731e92e947c6 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 20:15:41 +0100 Subject: adding verbiage and timing --- client/faceSearch/faceSearch.result.js | 52 +++++++++++++++++++++++----- megapixels/app/models/sql_factory.py | 2 +- megapixels/app/server/api.py | 62 ++++++++++++++++++++-------------- megapixels/app/site/parser.py | 2 +- site/assets/css/applets.css | 5 +++ site/public/test/index.html | 16 ++++----- site/public/test/style/index.html | 10 ++++-- 7 files changed, 104 insertions(+), 45 deletions(-) (limited to 'megapixels/app/models/sql_factory.py') diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index 1882def0..bc7831d9 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -4,31 +4,59 @@ import { connect } from 'react-redux' import { courtesyS } from '../util' import * as actions from './faceSearch.actions' +import { Loader } from '../common' const errors = { - 'bbox': "Sorry, we couldn't find a face in that image. Please choose an image where the face is large and clear.", - 'nomatch': "We didn't find a match.", - 'default': "There was an error!", + 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."} +
+ ), } class FaceSearchResult extends Component { render() { const { dataset } = this.props.payload - const { distances, results, error } = this.props.result + const { query, distances, results, loading, error } = this.props.result + if (loading) { + return ( +
+
+
+

Searching...

+
+
+ ) + } if (error) { - let errorMessage = errors[error.message] || errors.default + console.log(error) + let errorMessage = errors[error] || errors.error return (
{errorMessage}
) } if (!results) { return ( -
+
{errors.nomatch}
) } if (!this.props.result.results.length) { return ( -
No results
+
{errors.nomatch}
) } const els = results.map((result, i) => { @@ -48,7 +76,15 @@ class FaceSearchResult extends Component { 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/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 9a44941b..02b722df 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -98,7 +98,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() diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index bc60118c..b3447eb1 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -15,24 +15,32 @@ from app.utils.im_utils import pil2np sanitize_re = re.compile('[\W]+') valid_exts = ['.gif', '.jpg', '.jpeg', '.png'] +LIMIT = 9 +THRESHOLD = 0.3 + api = Blueprint('api', __name__) faiss_datasets = load_faiss_databases() @api.route('/') def index(): + """List the datasets and their fields""" return jsonify({ 'datasets': list_datasets() }) + @api.route('/dataset/') def show(name): + """Show the data that a dataset will return""" dataset = get_dataset(name) if dataset: return jsonify(dataset.describe()) else: return jsonify({ 'status': 404 }) + @api.route('/dataset//face/', methods=['POST']) def upload(name): + """Query an image against FAISS and return the matching identities""" start = time.time() dataset = get_dataset(name) if name not in faiss_datasets: @@ -52,31 +60,39 @@ def upload(name): im = Image.open(file.stream).convert('RGB') im_np = pil2np(im) - + # Face detection detector = face_detector.DetectorDLIBHOG() # get detection as BBox object bboxes = detector.detect(im_np, largest=True) - if not len(bboxes): + if not bboxes or not len(bboxes): return jsonify({ 'error': 'bbox' }) bbox = bboxes[0] + if not bbox: + return jsonify({ + 'error': 'bbox' + }) + dim = im_np.shape[:2][::-1] bbox = bbox.to_dim(dim) # convert back to real dimensions + print("got bbox") + if not bbox: + return jsonify({ + 'error': 'bbox' + }) - # face recognition/vector + # extract 128-D vector recognition = face_recognition.RecognitionDLIB(gpu=-1) vec = recognition.vec(im_np, bbox) - - # print(vec) query = np.array([ vec ]).astype('float32') - # query FAISS! - distances, indexes = faiss_dataset.search(query, 10) + # query FAISS + distances, indexes = faiss_dataset.search(query, LIMIT) - if len(indexes) == 0: + if len(indexes) == 0 or len(indexes[0]) == 0: return jsonify({ 'error': 'nomatch' }) @@ -85,36 +101,32 @@ def upload(name): distances = distances[0] indexes = indexes[0] - if len(indexes) == 0: - return jsonify({ - 'error': 'nomatch' - }) - - lookup = {} - ids = [i+1 for i in indexes] + dists = [] + ids = [] for _d, _i in zip(distances, indexes): - lookup[_i+1] = _d + if _d <= THRESHOLD: + dists.append(round(float(_d), 2)) + ids.append(_i+1) - print(distances) - print(indexes) + results = [ dataset.get_identity(_i) for _i in ids ] - # with the result we have an ID - # query the sql dataset for the UUID etc here + print(distances) + print(ids) query = { - 'timing': time.time() - start, + 'timing': round(time.time() - start, 3), } - results = [ dataset.get_identity(id) for id in ids ] - print(results) return jsonify({ + 'query': query, 'results': results, - 'distances': distances.tolist(), - 'indexes': indexes.tolist(), + 'distances': dists, }) + @api.route('/dataset//name', methods=['GET']) def name_lookup(dataset): + """Find a name in the dataset""" start = time.time() dataset = get_dataset(name) diff --git a/megapixels/app/site/parser.py b/megapixels/app/site/parser.py index ecfae0cb..b3d3a8c2 100644 --- a/megapixels/app/site/parser.py +++ b/megapixels/app/site/parser.py @@ -64,7 +64,7 @@ def format_applet(section, s3_path): else: command = payload[0] opt = None - if command == 'python': + if command == 'python' or command == 'javascript' or command == 'code': return format_section([ section ], s3_path) applet['command'] = command diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index ecba518c..edd5b709 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -18,6 +18,8 @@ } .results { + margin-top: 10px; + padding-bottom: 10px; display: flex; flex-direction: row; flex-wrap: wrap; @@ -27,6 +29,9 @@ margin-left: 20px; margin-bottom: 40px; font-size: 8pt; + background: #000; + padding: 5px; + font-weight: 500; } .results > div img { margin-bottom: 4px; diff --git a/site/public/test/index.html b/site/public/test/index.html index b4d16036..41f8eda5 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 6d99a236..ab13a589 100644 --- a/site/public/test/style/index.html +++ b/site/public/test/style/index.html @@ -54,10 +54,16 @@
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.

-
s = "Python syntax highlighting"
+
var s = "JavaScript syntax highlighting";
+alert(s);
+
+
s = "Python syntax highlighting"
 print(s)
 
-
tag."]}'>

Horizontal rule

+
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


Citations below here

-- cgit v1.2.3-70-g09d2 From 2194c77729202dfe58acb0de67bad7b65e3f7f5d Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 23:04:54 +0100 Subject: unicode --- megapixels/app/models/sql_factory.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'megapixels/app/models/sql_factory.py') diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 02b722df..c955b885 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -125,7 +125,7 @@ class SqlDataset: class UUID(self.base_model): __tablename__ = self.name + "_uuid" id = Column(Integer, primary_key=True) - uuid = Column(String(36), nullable=False) + uuid = Column(String(36, convert_unicode=True), nullable=False) def toJSON(self): return { 'id': self.id, @@ -167,9 +167,9 @@ class SqlDataset: class Identity(self.base_model): __tablename__ = self.name + "_identity" id = Column(Integer, primary_key=True) - fullname = Column(String(36), nullable=False) - description = Column(String(36), nullable=False) - gender = Column(String(1), nullable=False) + 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): -- cgit v1.2.3-70-g09d2 From ed7541f7e18ad8622ebecae588eace89608880c2 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 23:14:58 +0100 Subject: unicode --- megapixels/app/models/sql_factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'megapixels/app/models/sql_factory.py') diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index c955b885..cf652c6d 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -10,7 +10,7 @@ from sqlalchemy.ext.declarative import declarative_base from app.utils.file_utils import load_recipe, load_csv_safe from app.settings import app_cfg as cfg -connection_url = "mysql+mysqldb://{}:{}@{}/{}".format( +connection_url = "mysql+mysqldb://{}:{}@{}/{}?charset=utf8".format( os.getenv("DB_USER"), os.getenv("DB_PASS"), os.getenv("DB_HOST"), @@ -35,7 +35,7 @@ def load_sql_datasets(replace=False, base_model=None): global datasets, loaded, Session if loaded: return datasets - engine = create_engine(connection_url) + engine = create_engine(connection_url, encoding="utf-8") Session = sessionmaker(bind=engine) for path in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): dataset = load_sql_dataset(path, replace, engine, base_model) -- cgit v1.2.3-70-g09d2 From f2c7e5a9cabb5524fcd6fd9fb786a4223bbc7b1a Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 23:23:06 +0100 Subject: unicode --- megapixels/app/models/sql_factory.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'megapixels/app/models/sql_factory.py') diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index cf652c6d..414ef3a6 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -10,7 +10,7 @@ from sqlalchemy.ext.declarative import declarative_base from app.utils.file_utils import load_recipe, load_csv_safe from app.settings import app_cfg as cfg -connection_url = "mysql+mysqldb://{}:{}@{}/{}?charset=utf8".format( +connection_url = "mysql+mysqlconnector://{}:{}@{}/{}?charset=utf8mb4".format( os.getenv("DB_USER"), os.getenv("DB_PASS"), os.getenv("DB_HOST"), @@ -36,6 +36,11 @@ def load_sql_datasets(replace=False, base_model=None): if loaded: return datasets engine = create_engine(connection_url, encoding="utf-8") + # db.set_character_set('utf8') + # dbc = db.cursor() + # dbc.execute('SET NAMES utf8;') + # dbc.execute('SET CHARACTER SET utf8;') + # dbc.execute('SET character_set_connection=utf8;') Session = sessionmaker(bind=engine) for path in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): dataset = load_sql_dataset(path, replace, engine, base_model) -- cgit v1.2.3-70-g09d2 From 7b8e6f9a7d3eb36b72b53d5e754b9c7916b98ed7 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 18 Dec 2018 01:03:10 +0100 Subject: namesearchg --- client/actions.js | 4 +- client/applet.js | 5 +- client/faceSearch/faceSearch.container.js | 2 +- client/faceSearch/faceSearch.result.js | 16 +++++- client/index.js | 1 - client/nameSearch/index.js | 5 ++ client/nameSearch/nameSearch.actions.js | 52 ++++++++++++++++++ client/nameSearch/nameSearch.container.js | 24 +++++++++ client/nameSearch/nameSearch.query.js | 48 +++++++++++++++++ client/nameSearch/nameSearch.reducer.js | 32 +++++++++++ client/nameSearch/nameSearch.result.js | 88 +++++++++++++++++++++++++++++++ client/store.js | 8 +-- client/tables.js | 4 ++ client/types.js | 3 ++ megapixels/app/models/sql_factory.py | 14 +++++ megapixels/app/server/api.py | 41 +++++++------- megapixels/app/settings/app_cfg.py | 2 +- site/assets/css/applets.css | 24 +++++++-- 18 files changed, 339 insertions(+), 34 deletions(-) create mode 100644 client/nameSearch/index.js create mode 100644 client/nameSearch/nameSearch.actions.js create mode 100644 client/nameSearch/nameSearch.container.js create mode 100644 client/nameSearch/nameSearch.query.js create mode 100644 client/nameSearch/nameSearch.reducer.js create mode 100644 client/nameSearch/nameSearch.result.js (limited to 'megapixels/app/models/sql_factory.py') diff --git a/client/actions.js b/client/actions.js index bb011838..2be8229d 100644 --- a/client/actions.js +++ b/client/actions.js @@ -1,5 +1,7 @@ import * as faceSearch from './faceSearch/faceSearch.actions' +import * as nameSearch from './nameSearch/nameSearch.actions' export { - faceSearch + faceSearch, + nameSearch, } diff --git a/client/applet.js b/client/applet.js index 4d2a8e6c..80d40657 100644 --- a/client/applet.js +++ b/client/applet.js @@ -1,13 +1,16 @@ import React, { Component } from 'react' import { Container as FaceSearchContainer } from './faceSearch' +import { Container as NameSearchContainer } from './nameSearch' export default class Applet extends Component { render() { - console.log(this.props) + // console.log(this.props) switch (this.props.payload.cmd) { case 'face_search': return + case 'name_search': + return default: return
{'Megapixels'}
} diff --git a/client/faceSearch/faceSearch.container.js b/client/faceSearch/faceSearch.container.js index f96961db..94c6eb9f 100644 --- a/client/faceSearch/faceSearch.container.js +++ b/client/faceSearch/faceSearch.container.js @@ -10,7 +10,7 @@ import FaceSearchResult from './faceSearch.result' class FaceSearchContainer extends Component { render() { const { payload } = this.props - console.log(payload) + // console.log(payload) return (
diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index d63f3265..936bc8d2 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -32,6 +32,7 @@ class FaceSearchResult 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 (
@@ -43,7 +44,7 @@ class FaceSearchResult extends Component { ) } if (error) { - console.log(error) + // console.log(error) let errorMessage = errors[error] || errors.error return (
{errorMessage}
@@ -60,10 +61,21 @@ 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 { 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'}
diff --git a/client/index.js b/client/index.js index 2beb5526..93341a77 100644 --- a/client/index.js +++ b/client/index.js @@ -38,7 +38,6 @@ function appendApplets(applets) { el.classList.add('loaded') break default: - console.log('react', el, payload) appendReactApplet(el, payload) el.classList.add('loaded') break diff --git a/client/nameSearch/index.js b/client/nameSearch/index.js new file mode 100644 index 00000000..8c6475e4 --- /dev/null +++ b/client/nameSearch/index.js @@ -0,0 +1,5 @@ +import Container from './nameSearch.container' + +export { + Container, +} diff --git a/client/nameSearch/nameSearch.actions.js b/client/nameSearch/nameSearch.actions.js new file mode 100644 index 00000000..290ee38d --- /dev/null +++ b/client/nameSearch/nameSearch.actions.js @@ -0,0 +1,52 @@ +// import fetchJsonp from 'fetch-jsonp' +import * as types from '../types' +// import { hashPath } from '../util' +import { post } from '../util' +// import querystring from 'query-string' + +// urls + +const url = { + search: (dataset, q) => process.env.API_HOST + '/api/dataset/' + dataset + '/name?q=' + encodeURIComponent(q), +} +export const publicUrl = { +} + +// standard loading events + +const loading = (tag, offset) => ({ + type: types.nameSearch.loading, + tag, + offset +}) +const loaded = (tag, data, offset = 0) => ({ + type: types.nameSearch.loaded, + tag, + data, + offset +}) +const error = (tag, err) => ({ + type: types.nameSearch.error, + tag, + err +}) + +// search UI functions + +export const updateOptions = opt => dispatch => { + dispatch({ type: types.nameSearch.update_options, opt }) +} + +// API functions + +export const search = (payload, q) => dispatch => { + const tag = 'result' + const fd = new FormData() + fd.append('q', q) + dispatch(loading(tag)) + post(url.search(payload.dataset, q), fd) + .then(data => { + dispatch(loaded(tag, data)) + }) + .catch(err => dispatch(error(tag, err))) +} diff --git a/client/nameSearch/nameSearch.container.js b/client/nameSearch/nameSearch.container.js new file mode 100644 index 00000000..b0de0c3a --- /dev/null +++ b/client/nameSearch/nameSearch.container.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from './nameSearch.actions' + +import NameSearchQuery from './nameSearch.query' +import NameSearchResult from './nameSearch.result' + +class NameSearchContainer extends Component { + render() { + const { payload } = this.props + // console.log(payload) + return ( +
+ + +
+ ) + } +} + + +export default NameSearchContainer diff --git a/client/nameSearch/nameSearch.query.js b/client/nameSearch/nameSearch.query.js new file mode 100644 index 00000000..b82e324b --- /dev/null +++ b/client/nameSearch/nameSearch.query.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from './nameSearch.actions' + +class NameSearchQuery extends Component { + state = { + value: null + } + + handleInput(value) { + this.setState({ q: value }) + if (value.length > 2) { + this.props.actions.search(this.props.payload, value) + } + } + + render() { + return ( +
+

Find Your Name

+

Searching {13456} identities

+

+ {'Enter your name to see if you were included in this dataset..'} +

+ this.handleInput(e.target.value)} + /> +
+ ) + } +} + +const mapStateToProps = state => ({ + result: state.nameSearch.result, + options: state.nameSearch.options, +}) + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(NameSearchQuery) diff --git a/client/nameSearch/nameSearch.reducer.js b/client/nameSearch/nameSearch.reducer.js new file mode 100644 index 00000000..101c93ea --- /dev/null +++ b/client/nameSearch/nameSearch.reducer.js @@ -0,0 +1,32 @@ +import * as types from '../types' + +const initialState = () => ({ + query: {}, + result: {}, + loading: false, +}) + +export default function nameSearchReducer(state = initialState(), action) { + switch (action.type) { + case types.nameSearch.loading: + return { + ...state, + [action.tag]: { loading: true }, + } + + case types.nameSearch.loaded: + return { + ...state, + [action.tag]: action.data, + } + + case types.nameSearch.error: + return { + ...state, + [action.tag]: { error: action.err }, + } + + default: + return state + } +} diff --git a/client/nameSearch/nameSearch.result.js b/client/nameSearch/nameSearch.result.js new file mode 100644 index 00000000..9e20228c --- /dev/null +++ b/client/nameSearch/nameSearch.result.js @@ -0,0 +1,88 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { courtesyS } from '../util' + +import * as actions from './nameSearch.actions' +import { Loader } from '../common' + +const errors = { + nomatch: ( +
+

Name not found

+ {"No names matched your query."} +
+ ), + error: ( +
+

{"No matches found"}

+
+ ), +} + +class NameSearchResult extends Component { + render() { + const { dataset } = this.props.payload + const { query, results, loading, error } = this.props.result + console.log(this.props.result) + if (loading) { + return ( +
+
+ +
+
+ ) + } + 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 { uuid } = result.uuid + const { fullname, gender, description, images } = result.identity + return ( +
+ + {fullname} {'('}{gender}{')'}
+ {description}
+ {courtesyS(images, 'image')}{' in dataset'}
+
+ ) + }) + + return ( +
+
+ {'Search took '}{Math.round(query.timing * 1000) + ' ms'} +
+
+ {els} +
+
+ ) + } +} + +const mapStateToProps = state => ({ + query: state.nameSearch.query, + result: state.nameSearch.result, + options: state.nameSearch.options, +}) + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(NameSearchResult) diff --git a/client/store.js b/client/store.js index 03f983a5..13612f2d 100644 --- a/client/store.js +++ b/client/store.js @@ -1,16 +1,12 @@ import { applyMiddleware, compose, combineReducers, createStore } from 'redux' import thunk from 'redux-thunk' -// import metadataReducer from './metadata/metadata.reducer' import faceSearchReducer from './faceSearch/faceSearch.reducer' -// import reviewReducer from './review/review.reducer' +import nameSearchReducer from './nameSearch/nameSearch.reducer' const rootReducer = combineReducers({ - auth: (state = {}) => state, - // auth: (state = login()) => state, - // metadata: metadataReducer, faceSearch: faceSearchReducer, - // review: reviewReducer, + nameSearch: nameSearchReducer, }) function configureStore(initialState = {}) { diff --git a/client/tables.js b/client/tables.js index 2a2699f9..b4c13887 100644 --- a/client/tables.js +++ b/client/tables.js @@ -74,4 +74,8 @@ export default function append(el, payload) { } }) } + + if (fields.length > 1 && fields[1].indexOf('filter')) { + const filter = fields[1].split(' ') + } } diff --git a/client/types.js b/client/types.js index d295d0d1..fb1fbe30 100644 --- a/client/types.js +++ b/client/types.js @@ -10,5 +10,8 @@ export const faceSearch = tagAsType('faceSearch', [ 'loading', 'loaded', 'error', 'update_options', ]) +export const nameSearch = tagAsType('nameSearch', [ + 'loading', 'loaded', 'error', 'update_options', +]) export const init = '@@INIT' diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 414ef3a6..da95b539 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -97,6 +97,20 @@ class SqlDataset: 'pose': self.select('pose', id), } + def search_name(self, q): + table = self.get_table('identity_meta') + uuid_table = self.get_table('uuids') + + identity = table.query.filter(table.fullname.like(q)).order_by(table.fullname.desc()).limit(30) + identities = [] + for row in identity: + uuid = uuid_table.query.filter(uuid_table.id == row.image_id).first() + identities.append({ + 'uuid': uuid.toJSON(), + 'identity': row.toJSON(), + }) + return identities + def select(self, table, id): table = self.get_table(table) if not table: diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 8ff06611..33cf45df 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -28,26 +28,26 @@ def index(): return jsonify({ 'datasets': list_datasets() }) -@api.route('/dataset/') -def show(name): +@api.route('/dataset/') +def show(dataset_name): """Show the data that a dataset will return""" - dataset = get_dataset(name) + dataset = get_dataset(dataset_name) if dataset: return jsonify(dataset.describe()) else: return jsonify({ 'status': 404 }) -@api.route('/dataset//face/', methods=['POST']) -def upload(name): +@api.route('/dataset//face', methods=['POST']) +def upload(dataset_name): """Query an image against FAISS and return the matching identities""" start = time.time() - dataset = get_dataset(name) - if name not in faiss_datasets: + dataset = get_dataset(dataset_name) + if dataset_name not in faiss_datasets: return jsonify({ 'error': 'invalid dataset' }) - faiss_dataset = faiss_datasets[name] + faiss_dataset = faiss_datasets[dataset_name] file = request.files['query_img'] fn = file.filename if fn.endswith('blob'): @@ -106,17 +106,21 @@ def upload(name): for _d, _i in zip(distances, indexes): if _d <= THRESHOLD: dists.append(round(float(_d), 2)) - ids.append(_i) + ids.append(_i+1) results = [ dataset.get_identity(int(_i)) for _i in ids ] # print(distances) # print(ids) + # 'bbox': str(bboxes[0]), + # 'bbox_dim': str(bbox), + print(bboxes[0]) + print(bbox) + query = { - 'bbox': bboxes[0], - 'bbox_dim': bbox, 'timing': round(time.time() - start, 3), + 'bbox': str(bbox), } # print(results) return jsonify({ @@ -126,20 +130,21 @@ def upload(name): }) -@api.route('/dataset//name', methods=['GET']) -def name_lookup(dataset): +@api.route('/dataset//name', methods=['GET','POST']) +def name_lookup(dataset_name): """Find a name in the dataset""" start = time.time() - dataset = get_dataset(name) + dataset = get_dataset(dataset_name) - # we have a query from the request query string... - # use this to do a like* query on the identities_meta table + q = request.args.get('q') + print(q) query = { + 'q': q, 'timing': time.time() - start, } - results = [] - + results = dataset.search_name(q + '%') if q else None + # print(results) return jsonify({ 'query': query, diff --git a/megapixels/app/settings/app_cfg.py b/megapixels/app/settings/app_cfg.py index d7752739..55fed166 100644 --- a/megapixels/app/settings/app_cfg.py +++ b/megapixels/app/settings/app_cfg.py @@ -89,7 +89,7 @@ CKPT_ZERO_PADDING = 9 HASH_TREE_DEPTH = 3 HASH_BRANCH_SIZE = 3 -DLIB_FACEREC_JITTERS = 25 # number of face recognition jitters +DLIB_FACEREC_JITTERS = 5 # number of face recognition jitters DLIB_FACEREC_PADDING = 0.25 # default dlib POSE_MINMAX_YAW = (-25,25) diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index edd5b709..315d72e0 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -16,7 +16,15 @@ flex-direction: row; justify-content: flex-start; } - +.q { + width: 100%; + padding: 5px; + font-size: 14pt; +} +.timing { + font-size: 9pt; + padding-top: 10px; +} .results { margin-top: 10px; padding-bottom: 10px; @@ -34,9 +42,10 @@ font-weight: 500; } .results > div img { + display: block; margin-bottom: 4px; - width: 200px; - height: 200px; + width: 190px; + height: 190px; background: rgba(255,255,255,0.05); } .results > div:nth-child(3n+1) { @@ -45,6 +54,15 @@ .query h2 { margin-top: 0; padding-top: 0; } +.img { + position: relative; +} +.img .bbox { + position: absolute; + color: rgba(255,0,0,1); + background: rgba(255,0,0,0.05); + border: 1px solid; +} .cta { padding-left: 20px; font-size: 11pt; -- cgit v1.2.3-70-g09d2