From 7eb2b4509802388f2fe980a3477dad006cf81016 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 17:47:37 +0100 Subject: fix map --- client/map/index.js | 17 +++++++---------- site/assets/css/css.css | 1 + 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/client/map/index.js b/client/map/index.js index 788894f9..053cf13b 100644 --- a/client/map/index.js +++ b/client/map/index.js @@ -34,6 +34,7 @@ function addMarker(map, latlng, title, subtext) { subtext, ].join('')) } + function addArc(map, src, dest) { L.bezier({ path: [ @@ -46,14 +47,15 @@ function addArc(map, src, dest) { } export default function append(el, payload) { - const { cmd, data } = payload + const { data } = payload + let { paper, address } = data + let source = [0, 0] const citations = getCitations(data) - // console.log(el) let map = L.map(el).setView([25, 0], 2) L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', { - attribution: 'Map data © OpenStreetMap contributors,' + - 'CC-BY-SA,' + + attribution: 'Map data © OpenStreetMap contributors, ' + + 'CC-BY-SA, ' + 'Imagery © Mapbox', maxZoom: 18, id: 'mapbox.dark', @@ -61,13 +63,8 @@ export default function append(el, payload) { accessToken: 'pk.eyJ1IjoiZmFuc2FsY3kiLCJhIjoiY2pvN3I1czJwMHF5NDNrbWRoMWpteHlrdCJ9.kMpM5syQUhVjKkn1iVx9fg' }).addTo(map) - let { address } = data - console.log(address) - let source = [0, 0] if (address) { source = address.slice(3, 5).map(n => parseFloat(n)) - // console.log(map, address, source) - console.log(source) } citations.forEach(point => { @@ -77,5 +74,5 @@ export default function append(el, payload) { addArc(map, source, latlng) }) - addMarker(map, source, document.querySelector('h2').innerText, address[0]) + addMarker(map, source, paper.title, paper.address) } diff --git a/site/assets/css/css.css b/site/assets/css/css.css index 4f2d7c6e..003ac4a3 100644 --- a/site/assets/css/css.css +++ b/site/assets/css/css.css @@ -27,6 +27,7 @@ header { left: 0; width: 100%; height: 70px; + z-index: 1; background: #1e1e1e; display: flex; flex-direction: row; -- 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(-) 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 e67871d26f2e73861187e86110e240dd7718ea51 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 20:17:38 +0100 Subject: percentage --- client/faceSearch/faceSearch.result.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index bc7831d9..2a85552d 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -64,12 +64,12 @@ class FaceSearchResult extends Component { const { uuid } = result.uuid const { fullname, gender, description, images } = result.identity return ( -
+
{fullname} {'('}{gender}{')'}
{description}
- {courtesyS(images, 'image')}
- {distance} + {courtesyS(images, 'image')}{' in dataset'}
+ {Math.round((1 - distance) * 100)}{'% match'}
) }) -- cgit v1.2.3-70-g09d2 From bd1da3b62badaaff27b0d5f6a0b7553056445867 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 23:00:58 +0100 Subject: webpack cfg --- README.md | 8 ++++++++ webpack.config.dev.js | 13 +------------ webpack.config.prod.js | 3 +-- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5d17c304..b3985f18 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,14 @@ FaceQuery.me, mozilla, nytimes - nvm, node ``` +conda install pytorch torchvision -c pytorch +conda install faiss-cpu -c pytorch +pip install numpy Pillow +pip install dlib +pip install requests simplejson click pdfminer.six +pip install urllib3 flask flask_sqlalchemy +pip install pymediainfo tqdm opencv-python imutils scikit-image python-dotenv imagehash scikit-learn colorlog + sudo apt-get install libmysqlclient-dev mkdir -p /data_store_hdd/apps/megapixels/faiss/indexes diff --git a/webpack.config.dev.js b/webpack.config.dev.js index d6f7af46..4137a948 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -1,6 +1,6 @@ require('dotenv').config() -const HtmlWebpackPlugin = require('html-webpack-plugin') +// const HtmlWebpackPlugin = require('html-webpack-plugin') // const CleanWebpackPlugin = require('clean-webpack-plugin') const webpack = require('webpack') const path = require('path') @@ -13,19 +13,9 @@ module.exports = { path: path.resolve(__dirname, 'site/assets/js/dist'), filename: 'index.js' }, - // devServer: { - // port: 9000, - // headers: { - // 'Access-Control-Allow-Origin': '*', - // }, - // publicPath: '/site/assets/js/dist/', - // hot: true, - // }, devtool: 'inline-source-map', resolve: { alias: { - // 'vcat-header': path.resolve(__dirname, '../app/components/common/header.component.js'), - // 'vcat-auth-reducer': path.resolve(__dirname, '../app/reducers/auth.reducer.js'), "react": "preact-compat", "react-dom": "preact-compat" } @@ -35,7 +25,6 @@ module.exports = { new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"', 'process.env.S3_HOST': '"' + process.env.S3_HOST + '"', - // 'process.env.VCAT_HOST': '"http://127.0.0.1:8000"', 'process.env.API_HOST': '""', }), // new HtmlWebpackPlugin({ diff --git a/webpack.config.prod.js b/webpack.config.prod.js index b9d3f411..f5da2bb3 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -17,8 +17,7 @@ module.exports = { new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"', 'process.env.S3_HOST': '"' + process.env.S3_HOST + '"', - // 'process.env.VCAT_HOST': '""', - // 'process.env.API_HOST': '"https://syrianarchive.vframe.io"', + 'process.env.API_HOST': '""', }), new UglifyJsPlugin(), new webpack.optimize.AggressiveMergingPlugin() -- 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(-) 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(-) 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(-) 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 8d20a79ede9a3e9b0dd76a0f25a0118e9408e38e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 23:23:59 +0100 Subject: unicode --- megapixels/app/settings/app_cfg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/megapixels/app/settings/app_cfg.py b/megapixels/app/settings/app_cfg.py index 0c28b315..d7752739 100644 --- a/megapixels/app/settings/app_cfg.py +++ b/megapixels/app/settings/app_cfg.py @@ -7,6 +7,8 @@ from dotenv import load_dotenv from app.settings import types from app.utils import click_utils +import codecs +codecs.register(lambda name: codecs.lookup('utf8') if name == 'utf8mb4' else None) # ----------------------------------------------------------------------------- # Enun lists used for custom Click Params -- cgit v1.2.3-70-g09d2 From b0120413faebeffb41db151d82b07775c754ad15 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 23:36:25 +0100 Subject: unicode --- README.md | 8 ++++++++ megapixels/app/server/api.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b3985f18..5c07fa75 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,14 @@ mkdir -p /data_store_hdd/apps/megapixels/faiss/indexes mkdir -p /data_store_hdd/apps/megapixels/faiss/metadata ``` +### MySQL note + +You may need to set the database charset to `utf8mb4` in order to import the CSVs: + +``` +ALTER DATABASE megapixels CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; +``` + ## Building the site ``` diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index b3447eb1..024c32cb 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -108,7 +108,7 @@ def upload(name): dists.append(round(float(_d), 2)) ids.append(_i+1) - results = [ dataset.get_identity(_i) for _i in ids ] + results = [ dataset.get_identity(int(_i)) for _i in ids ] print(distances) print(ids) -- cgit v1.2.3-70-g09d2 From dfce989731fc58268b40280af79eeaa0b80b333e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 23:49:57 +0100 Subject: unicode --- client/faceSearch/faceSearch.result.js | 6 ++---- megapixels/app/server/api.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index 2a85552d..d63f3265 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -50,11 +50,9 @@ class FaceSearchResult extends Component { ) } if (!results) { - return ( -
{errors.nomatch}
- ) + return
} - if (!this.props.result.results.length) { + if (!results.length) { return (
{errors.nomatch}
) diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 024c32cb..9c874d81 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -54,7 +54,7 @@ def upload(name): fn = 'filename.jpg' basename, ext = os.path.splitext(fn) - print("got {}, type {}".format(basename, ext)) + # print("got {}, type {}".format(basename, ext)) if ext.lower() not in valid_exts: return jsonify({ 'error': 'not an image' }) @@ -78,7 +78,7 @@ def upload(name): dim = im_np.shape[:2][::-1] bbox = bbox.to_dim(dim) # convert back to real dimensions - print("got bbox") + # print("got bbox") if not bbox: return jsonify({ 'error': 'bbox' @@ -110,13 +110,15 @@ def upload(name): results = [ dataset.get_identity(int(_i)) for _i in ids ] - print(distances) - print(ids) + # print(distances) + # print(ids) query = { + 'bbox': bboxes[0], + 'bbox_dim': bbox, 'timing': round(time.time() - start, 3), } - print(results) + # print(results) return jsonify({ 'query': query, 'results': results, @@ -138,7 +140,7 @@ def name_lookup(dataset): } results = [] - print(results) + # print(results) return jsonify({ 'query': query, 'results': results, -- cgit v1.2.3-70-g09d2 From d3be915bc5725a36dee867b07404725177783460 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 23:53:18 +0100 Subject: unicode --- README.md | 5 +++-- megapixels/app/server/api.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5c07fa75..ed692e40 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,9 @@ conda install faiss-cpu -c pytorch pip install numpy Pillow pip install dlib pip install requests simplejson click pdfminer.six -pip install urllib3 flask flask_sqlalchemy -pip install pymediainfo tqdm opencv-python imutils scikit-image python-dotenv imagehash scikit-learn colorlog +pip install urllib3 flask flask_sqlalchemy mysql-connector +pip install pymediainfo tqdm opencv-python imutils +pip install scikit-image python-dotenv imagehash scikit-learn colorlog sudo apt-get install libmysqlclient-dev diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 9c874d81..8ff06611 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -106,7 +106,7 @@ def upload(name): for _d, _i in zip(distances, indexes): if _d <= THRESHOLD: dists.append(round(float(_d), 2)) - ids.append(_i+1) + ids.append(_i) results = [ dataset.get_identity(int(_i)) for _i in ids ] -- 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 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 From 66d915e8c84c50572a3e575a62d40c3543598c6e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 18 Dec 2018 01:05:43 +0100 Subject: bbox --- megapixels/app/server/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 33cf45df..96fedeee 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -115,8 +115,8 @@ def upload(dataset_name): # 'bbox': str(bboxes[0]), # 'bbox_dim': str(bbox), - print(bboxes[0]) - print(bbox) + # print(bboxes[0]) + # print(bbox) query = { 'timing': round(time.time() - start, 3), -- cgit v1.2.3-70-g09d2 From f5090a87d6261216fd7d6842d0337753bbd3c918 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 18 Dec 2018 01:08:05 +0100 Subject: bbox --- megapixels/app/server/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 96fedeee..805b24d4 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -120,7 +120,7 @@ def upload(dataset_name): query = { 'timing': round(time.time() - start, 3), - 'bbox': str(bbox), + # 'bbox': str(bbox), } # print(results) return jsonify({ -- cgit v1.2.3-70-g09d2 From ebafdcb75d7f3c440701be952f58b8b08c12d25c Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 18 Dec 2018 01:12:10 +0100 Subject: bbox --- megapixels/app/server/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 805b24d4..9c0527c1 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -54,7 +54,7 @@ def upload(dataset_name): fn = 'filename.jpg' basename, ext = os.path.splitext(fn) - # print("got {}, type {}".format(basename, ext)) + api.logger.info("got {}, type {}".format(basename, ext)) if ext.lower() not in valid_exts: return jsonify({ 'error': 'not an image' }) @@ -78,7 +78,7 @@ def upload(dataset_name): dim = im_np.shape[:2][::-1] bbox = bbox.to_dim(dim) # convert back to real dimensions - # print("got bbox") + api.logger.info("got bbox") if not bbox: return jsonify({ 'error': 'bbox' @@ -122,7 +122,7 @@ def upload(dataset_name): 'timing': round(time.time() - start, 3), # 'bbox': str(bbox), } - # print(results) + api.logger.info(results) return jsonify({ 'query': query, 'results': results, @@ -137,7 +137,7 @@ def name_lookup(dataset_name): dataset = get_dataset(dataset_name) q = request.args.get('q') - print(q) + # print(q) query = { 'q': q, -- cgit v1.2.3-70-g09d2 From f9810daea17a90bfe40241719c835e5484d7403d Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 18 Dec 2018 01:13:42 +0100 Subject: bbox --- client/faceSearch/faceSearch.actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/faceSearch/faceSearch.actions.js b/client/faceSearch/faceSearch.actions.js index 224977b5..03e1a91d 100644 --- a/client/faceSearch/faceSearch.actions.js +++ b/client/faceSearch/faceSearch.actions.js @@ -8,7 +8,7 @@ import { post, preloadImage } from '../util' // urls const url = { - upload: (dataset) => process.env.API_HOST + '/api/dataset/' + dataset + '/face/', + upload: (dataset) => process.env.API_HOST + '/api/dataset/' + dataset + '/face', } export const publicUrl = { } -- cgit v1.2.3-70-g09d2 From bf3dd1399e4ef1db5fb8830004827fe603f73b2e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 18 Dec 2018 01:15:38 +0100 Subject: bbox --- megapixels/app/server/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 9c0527c1..35862837 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -54,7 +54,7 @@ def upload(dataset_name): fn = 'filename.jpg' basename, ext = os.path.splitext(fn) - api.logger.info("got {}, type {}".format(basename, ext)) + # print("got {}, type {}".format(basename, ext)) if ext.lower() not in valid_exts: return jsonify({ 'error': 'not an image' }) @@ -78,7 +78,7 @@ def upload(dataset_name): dim = im_np.shape[:2][::-1] bbox = bbox.to_dim(dim) # convert back to real dimensions - api.logger.info("got bbox") + # print("got bbox") if not bbox: return jsonify({ 'error': 'bbox' @@ -120,9 +120,9 @@ def upload(dataset_name): query = { 'timing': round(time.time() - start, 3), - # 'bbox': str(bbox), + 'bbox': str(bbox), } - api.logger.info(results) + # print(results) return jsonify({ 'query': query, 'results': results, -- cgit v1.2.3-70-g09d2