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 + 16 files changed, 1102 insertions(+) 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 (limited to 'faiss') 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() + -- cgit v1.2.3-70-g09d2 From 485cf0e4665c660d4e5e1fba00a95bc8036809c6 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sat, 15 Dec 2018 16:40:34 +0100 Subject: db stuff --- faiss/__init__.py | 0 faiss/server.py | 68 ----------------- megapixels/app/server/api.py | 97 +++++++++---------------- site/public/datasets/lfw/index.html | 67 +++++++++++++---- site/public/datasets/vgg_face2/index.html | 84 +++++++++++++++++++++ site/public/index.html | 1 + site/public/research/00_introduction/index.html | 86 ++++++++++++++++++++++ site/public/research/index.html | 2 +- 8 files changed, 259 insertions(+), 146 deletions(-) delete mode 100644 faiss/__init__.py delete mode 100644 faiss/server.py create mode 100644 site/public/datasets/vgg_face2/index.html create mode 100644 site/public/research/00_introduction/index.html (limited to 'faiss') diff --git a/faiss/__init__.py b/faiss/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/faiss/server.py b/faiss/server.py deleted file mode 100644 index a8c660fa..00000000 --- a/faiss/server.py +++ /dev/null @@ -1,68 +0,0 @@ -#!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/megapixels/app/server/api.py b/megapixels/app/server/api.py index 428c53b1..c5e27dd2 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -1,30 +1,13 @@ -from flask import Blueprint, jsonify +import os +import re +import time +from flask import Blueprint, request, jsonify +from PIL import Image # todo: try to remove PIL dependency 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 +sanitize_re = re.compile('[\W]+') +valid_exts = ['.gif', '.jpg', '.jpeg', '.png'] api = Blueprint('api', __name__) @@ -40,40 +23,32 @@ def show(name): else: return jsonify({ 'status': 404 }) -@api.route('/dataset//test', methods=['POST']) -def test(name): - print('hiiiiii') - return jsonify({ 'test': 'OK', 'dataset': name }) - -# @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, -# }) +@api.route('/dataset//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' }) + + img = Image.open(file.stream).convert('RGB') + + # 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, + } + results = [] + + print(results) + return jsonify({ + 'query': query, + 'results': results, + }) diff --git a/site/public/datasets/lfw/index.html b/site/public/datasets/lfw/index.html index 39052b44..e080229f 100644 --- a/site/public/datasets/lfw/index.html +++ b/site/public/datasets/lfw/index.html @@ -26,23 +26,50 @@
-

Labeled Faces in The Wild

-
Created
2007
Images
13,233
People
5,749
Created From
Yahoo News images
Search available
Searchable

Labeled Faces in The Wild (LFW) is amongst the most widely used facial recognition training datasets in the world and is the first of its kind to be created entirely from images that were posted online. The LFW dataset includes 13,233 images of 5,749 people that were collected between 2002-2004. Use the tools below to check if you were included in this dataset or scroll down to read the analysis.

+

Labeled Faces in the Wild

+
Created
2007
Images
13,233
People
5,749
Created From
Yahoo News images
Search available
Searchable

Labeled Faces in The Wild (LFW) is amongst the most widely used facial recognition training datasets in the world and is the first of its kind to be created entirely from images posted online. The LFW dataset includes 13,233 images of 5,749 people that were collected between 2002-2004. Use the tools below to check if you were included in this dataset or scroll down to read the analysis.

{INSERT IMAGE SEARCH MODULE}

{INSERT TEXT SEARCH MODULE}

-
Eight out of 5,749 people in the Labeled Faces in the Wild dataset. The face recognition training dataset is created entirely from photos downloaded from the Internet.
Eight out of 5,749 people in the Labeled Faces in the Wild dataset. The face recognition training dataset is created entirely from photos downloaded from the Internet.

INTRO

-

It began in 2002. Researchers at University of Massachusetts Amherst were developing algorithms for facial recognition and they needed more data. Between 2002-2004 they scraped Yahoo News for images of public figures. Two years later they cleaned up the dataset and repackaged it as Labeled Faces in the Wild (LFW).

-

Since then the LFW dataset has become one of the most widely used datasets used for evaluating face recognition algorithms. The associated research paper “Labeled Faces in the Wild: A Database for Studying Face Recognition in Unconstrained Environments” has been cited 996 times reaching 45 different countries throughout the world.

-

The faces come from news stories and are mostly celebrities from the entertainment industry, politicians, and villains. It’s a sampling of current affairs and breaking news that has come to pass. The images, detached from their original context now server a new purpose: to train, evaluate, and improve facial recognition.

-

As the most widely used facial recognition dataset, it can be said that each individual in LFW has, in a small way, contributed to the current state of the art in facial recognition surveillance. John Cusack, Julianne Moore, Barry Bonds, Osama bin Laden, and even Moby are amongst these biometric pillars, exemplar faces provided the visual dimensions of a new computer vision future.

-
The entire LFW dataset cropped to facial regions
The entire LFW dataset cropped to facial regions

In addition to commercial use as an evaluation tool, alll of the faces in LFW dataset are prepackaged into a popular machine learning code framework called scikit-learn.

-

Facts

-

The person with the most images is: -The person with the least images is:

-

Commercial Use

+
load file: lfw_names_gender_kg_min.csv
+Name, Images, Gender, Description
+
+
Eighteen of the 5,749 people in the Labeled Faces in the Wild Dataset. The most widely used face dataset for benchmarking commercial face recognition algorithms.
Eighteen of the 5,749 people in the Labeled Faces in the Wild Dataset. The most widely used face dataset for benchmarking commercial face recognition algorithms.

Intro

+

Three paragraphs describing the LFW dataset in a format that can be easily replicated for the other datasets. Nothing too custom. An analysis of the initial research papers with context relative to all the other dataset papers.

+
 all 5,749 people in the LFW Dataset sorted from most to least images collected.
all 5,749 people in the LFW Dataset sorted from most to least images collected.

LFW by the Numbers

+
    +
  • Was first published in 2007
  • +
  • Developed out of a prior dataset from Berkely called "Faces in the Wild" or "Names and Faces" [^lfw_original_paper]
  • +
  • Includes 13,233 images and 5,749 different people [^lfw_website]
  • +
  • There are about 3 men for every 1 woman (4,277 men and 1,472 women)[^lfw_website]
  • +
  • The person with the most images is George W. Bush with 530
  • +
  • Most people (70%) in the dataset have only 1 image
  • +
  • Thre are 1,680 people in the dataset with 2 or more images [^lfw_website]
  • +
  • Two out of 4 of the original authors received funding from the Office of Director of National Intelligence and IARPA for their 2016 LFW survey follow up report
  • +
  • The LFW dataset includes over 500 actors, 30 models, 10 presidents, 24 football players, 124 basketball players, 11 kings, and 2 queens
  • +
  • In all the LFW publications provided by the authors the words "ethics", "consent", and "privacy" appear 0 times [^lfw_original_paper], [^lfw_survey], [^lfw_tech_report] , [^lfw_website]
  • +
  • The word "future" appears 71 times
  • +
+

Facts

+
    +
  • Was created for the purpose of improving "unconstrained face recognition" [^lfw_original_paper]
  • +
  • All images in LFW were obtained "in the wild" meaning without any consent from the subject or from the photographer
  • +
  • The faces were detected using the Viola-Jones haarcascade face detector [^lfw_website] [^lfw_survey]
  • +
  • Is considered the "most popular benchmark for face recognition" [^lfw_baidu]
  • +
  • Is "the most widely used evaluation set in the field of facial recognition" [^lfw_pingan]
  • +
  • Is used by several of the largest tech companies in the world including "Google, Facebook, Microsoft Research Asia, Baidu, Tencent, SenseTime, Face++ and Chinese University of Hong Kong." [^lfw_pingan]
  • +
+

need citations

+
    +
  • All images were copied from Yahoo News between 2002 - 2004 [^lfw_original_paper]
  • +
  • SenseTime, who has relied on LFW for benchmarking their facial recognition performance, is the leading provider of surveillance to the Chinese Government (need citation)
  • +
+
 former President George W. Bush
former President George W. Bush
+
 Colin Powel (236), Tony Blair (144), and Donald Rumsfeld (121)
Colin Powel (236), Tony Blair (144), and Donald Rumsfeld (121)

People and Companies using the LFW Dataset

+

This section describes who is using the dataset and for what purposes. It should include specific examples of people or companies with citations and screenshots. This section is followed up by the graph, the map, and then the supplementary material.

The LFW dataset is used by numerous companies for benchmarking algorithms and in some cases training. According to the benchmarking results page [^lfw_results] provided by the authors, over 2 dozen companies have contributed their benchmark results.

According to BiometricUpdate.com [^lfw_pingan], LFW is "the most widely used evaluation set in the field of facial recognition, LFW attracts a few dozen teams from around the globe including Google, Facebook, Microsoft Research Asia, Baidu, Tencent, SenseTime, Face++ and Chinese University of Hong Kong."

According to researchers at the Baidu Research – Institute of Deep Learning "LFW has been the most popular evaluation benchmark for face recognition, and played a very important role in facilitating the face recognition society to improve algorithm. [^lfw_baidu]."

+

In addition to commercial use as an evaluation tool, alll of the faces in LFW dataset are prepackaged into a popular machine learning code framework called scikit-learn.

load file: lfw_commercial_use.csv
 name_display,company_url,example_url,country,description
 
@@ -83,13 +110,18 @@ name_display,company_url,example_url,country,description

The LFW face recognition training and evaluation dataset is a historically important face dataset as it was the first popular dataset to be created entirely from Internet images, paving the way for a global trend towards downloading anyone’s face from the Internet and adding it to a dataset. As will be evident with other datasets, LFW’s approach has now become the norm.

For all the 5,000 people in this datasets, their face is forever a part of facial recognition history. It would be impossible to remove anyone from the dataset because it is so ubiquitous. For their rest of the lives and forever after, these 5,000 people will continue to be used for training facial recognition surveillance.

Right to Removal

-

If you are affected by disclosure of your identity in this dataset please do contact the authors, many state that they are willing to remove images upon request. The authors of the LFW can be reached from the emails posted in their paper:

+

If you are affected by disclosure of your identity in this dataset please do contact the authors. Many have stated that they are willing to remove images upon request. The authors of the LFW dataset provide the following email for inquiries:

You can use the following message to request removal from the dataset:

+

To: Gary Huang mailto:gbhuang@cs.umass.edu

+

Subject: Request for Removal from LFW Face Dataset

Dear [researcher name],

-

I am writing to you about the "LFW Dataset". Recently I have discovered that your dataset includes my identity and no longer wish to be included in your dataset

-

MegaPixels is an educational art project developed for academic purposes. In no way does this project aim to villify the researchers who produced the datasets. The aim of this project is to encourage discourse around ethics and consent in artificial intelligence by providing information about these datasets that is otherwise difficult to obtain or inaccessible to other researchers.

+

I am writing to you about the "Labeled Faces in The Wild Dataset". Recently I discovered that your dataset includes my identity and I no longer wish to be included in your dataset.

+

The dataset is being used thousands of companies around the world to improve facial recognition software including usage by governments for the purpose of law enforcement, national security, tracking consumers in retail environments, and tracking individuals through public spaces.

+

My name as it appears in your dataset is [your name]. Please remove all images from your dataset and inform your newsletter subscribers to likewise update their copies.

+

- [your name]

+

Supplementary Data

-

Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam eaque ipsa, quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt, explicabo. Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos.

+

Researchers, journ

@@ -221,6 +253,9 @@ imageio.imwrite('lfw_montage_full.png', montage) montage_960 = imutils.resize(montage, width=960) imageio.imwrite('lfw_montage_960.jpg', montage_960) +

Disclaimer

+

MegaPixels is an educational art project designed to encourage discourse about facial recognition datasets. Any ethical or legal issues should be directed to the researcher's parent organizations. Except where necessary for contact or clarity, the names of researchers have been subsituted by their parent organization. In no way does this project aim to villify researchers who produced the datasets.

+

Read more about MegaPixels Code of Conduct


    diff --git a/site/public/datasets/vgg_face2/index.html b/site/public/datasets/vgg_face2/index.html new file mode 100644 index 00000000..24a1059b --- /dev/null +++ b/site/public/datasets/vgg_face2/index.html @@ -0,0 +1,84 @@ + + + + MegaPixels + + + + + + + + + +
    + + +
    MegaPixels
    + The Darkside of Datasets +
    + +
    +
    + +

    VGG Faces2

    +
    Created
    2018
    Images
    3.3M
    People
    9,000
    Created From
    Scraping search engines
    Search available
    [Searchable](#)

    VGG Face2 is the updated version of the VGG Face dataset and now includes over 3.3M face images from over 9K people. The identities were selected by taking the top 500K identities in Google's Knowledge Graph of celebrities and then selecting only the names that yielded enough training images. The dataset was created in the UK but funded by Office of Director of National Intelligence in the United States.

    +

    {INSERT IMAGE SEARCH MODULE}

    +

    {INSERT TEXT SEARCH MODULE}

    +
    load file: lfw_names_gender_kg_min.csv
    +Name, Images, Gender, Description
    +
    +

    VGG Face2 by the Numbers

    +
      +
    • 1,331 actresses, 139 presidents
    • +
    • 3 husbands and 16 wives
    • +
    • 2 snooker player
    • +
    • 1 guru
    • +
    • 1 pornographic actress
    • +
    • 3 computer programmer
    • +
    +

    Names and descriptions

    +
      +
    • The original VGGF2 name list has been updated with the results returned from Google Knowledge
    • +
    • Names with a similarity score greater than 0.75 where automatically updated. Scores computed using import difflib; seq = difflib.SequenceMatcher(a=a.lower(), b=b.lower()); score = seq.ratio()
    • +
    • The 97 names with a score of 0.75 or lower were manually reviewed and includes name changes validating using Wikipedia.org results for names such as "Bruce Jenner" to "Caitlyn Jenner", spousal last-name changes, and discretionary changes to improve search results such as combining nicknames with full name when appropriate, for example changing "Aleksandar Petrović" to "Aleksandar 'Aco' Petrović" and minor changes such as "Mohammad Ali" to "Muhammad Ali"
    • +
    • The 'Description` text was automatically added when the Knowledge Graph score was greater than 250
    • +
    +

    TODO

    +
      +
    • create name list, and populate with Knowledge graph information like LFW
    • +
    • make list of interesting number stats, by the numbers
    • +
    • make list of interesting important facts
    • +
    • write intro abstract
    • +
    • write analysis of usage
    • +
    • find examples, citations, and screenshots of useage
    • +
    • find list of companies using it for table
    • +
    • create montages of the dataset, like LFW
    • +
    • create right to removal information
    • +
    +
    + +
    + + + + + \ No newline at end of file diff --git a/site/public/index.html b/site/public/index.html index 51006b59..91ff467a 100644 --- a/site/public/index.html +++ b/site/public/index.html @@ -71,6 +71,7 @@ + diff --git a/site/public/research/00_introduction/index.html b/site/public/research/00_introduction/index.html new file mode 100644 index 00000000..8f598f5b --- /dev/null +++ b/site/public/research/00_introduction/index.html @@ -0,0 +1,86 @@ + + + + MegaPixels + + + + + + + + + +
    + + +
    MegaPixels
    + The Darkside of Datasets +
    + +
    +
    + +
    +

    Untitled Page

    +
    +
    +
    Posted
    +
    2018-12-31
    +
    +
    +
    By
    +
    Adam Harvey
    +
    + +
    +
    + +

    It was the early 2000s. Face recognition was new and no one seemed sure exactly how well it was going to perform in practice. In theory, face recognition was poised to be a game changer, a force multiplier, a strategic military advantage, a way to make cities safer and to secure borders. This was the future John Ashcroft demanded with the Total Information Awareness act of the 2003 and that spooks had dreamed of for decades. It was a future that academics at Carnegie Mellon Universtiy and Colorado State University would help build. It was also a future that celebrities would play a significant role in building. And to the surprise of ordinary Internet users like myself and perhaps you, it was a future that millions of Internet users would unwittingly play role in creating.

    +

    Now the future has arrived and it doesn't make sense. Facial recognition works yet it doesn't actually work. Facial recognition is cheap and accessible but also expensive and out of control. Facial recognition research has achieved headline grabbing superhuman accuracies over 99.9% yet facial recognition is also dangerously inaccurate. During a trial installation at Sudkreuz station in Berlin in 2018, 20% of the matches were wrong, a number so low that it should not have any connection to law enforcement or justice. And in London, the Metropolitan police had been using facial recognition software that mistakenly identified an alarming 98% of people as criminals 1, which perhaps is a crime itself.

    +

    MegaPixels is an online art project that explores the history of facial recognition from the perspective of datasets. To paraphrase the artist Trevor Paglen, whoever controls the dataset controls the meaning. MegaPixels aims to unravel the meanings behind the data and expose the darker corners of the biometric industry that have contributed to its growth. MegaPixels does not start with a conclusion, a moralistic slant, or a

    +

    Whether or not to build facial recognition was a question that can no longer be asked. As an outspoken critic of face recognition I've developed, and hopefully furthered, my understanding during the last 10 years I've spent working with computer vision. Though I initially disagreed, I've come to see technocratic perspective as a non-negotiable reality. As Oren (nytimes article) wrote in NYT Op-Ed "the horse is out of the barn" and the only thing we can do collectively or individually is to steer towards the least worse outcome. Computational communication has entered a new era and it's both exciting and frightening to explore the potentials and opportunities. In 1997 getting access to 1 teraFLOPS of computational power would have cost you $55 million and required a strategic partnership with the Department of Defense. At the time of writing, anyone can rent 1 teraFLOPS on a cloud GPU marketplace for less than $1/day. 2.

    +

    I hope that this project will illuminate the darker areas of strange world of facial recognition that have not yet received attention and encourage discourse in academic, industry, and . By no means do I believe discourse can save the day. Nor do I think creating artwork can. In fact, I'm not exactly sure what the outcome of this project will be. The project is not so much what I publish here but what happens after. This entire project is only a prologue.

    +

    As McLuhan wrote, "You can't have a static, fixed position in the electric age". And in our hyper-connected age of mass surveillance, artificial intelligece, and unevenly distributed virtual futures the most irrational thing to be is rational. Increasingly the world is becoming a contradiction where people use surveillance to protest surveillance, use

    +

    Like many projects, MegaPixels had spent years meandering between formats, unfeasible budgets, and was generally too niche of a subject. The basic idea for this project, as proposed to the original Glass Room installation in 2016 in NYC, was to build an interactive mirror that showed people if they had been included in the LFW facial recognition dataset. The idea was based on my reaction to all the datasets I'd come across during research for the CV Dazzle project. I'd noticed strange datasets created for training and testing face detection algorithms. Most were created in labratory settings and their interpretation of face data was very strict.

    +

    About the name

    +

    About the funding

    +

    About me

    +

    About the team

    +

    Conclusion

    +

    for other post

    +

    It was the early 2000s. Face recognition was new and no one seemed sure how well it was going to perform in practice. In theory, face recognition was poised to be a game changer, a force multiplier, a strategic military advantage, a way to make cities safer and to secure the borders. It was the future that John Ashcroft demanded with the Total Information Awareness act of the 2003. It was a future that academics helped build. It was a future that celebrities helped build. And it was a future that

    +

    A decade earlier the Department of Homeland Security and the Counterdrug Technology Development Program Office initated a feasibilty study called FERET (FacE REcognition Technology) to "develop automatic face recognition capabilities that could be employed to assist security, intelligence, and law enforcement personnel in the performance of their duties [^feret_website]."

    +

    One problem with FERET dataset was that the photos were in controlled settings. For face recognition to work it would have to be used in uncontrolled settings. Even newer datasets such as the Multi-PIE (Pose, Illumination, and Expression) from Carnegie Mellon University included only indoor photos of cooperative subjects. Not only were the photos completely unrealistic, CMU's Multi-Pie included only 18 individuals and cost $500 for academic use [^cmu_multipie_cost], took years to create, and required consent from every participant.

    +
    +
    +
    1. Sharman, Jon. "Metropolitan Police's facial recognition technology 98% inaccurate, figures show". 2018. https://www.independent.co.uk/news/uk/home-news/met-police-facial-recognition-success-south-wales-trial-home-office-false-positive-a8345036.html

    2. +
    3. Calle, Dan. "Supercomptuers". 1997. http://ei.cs.vt.edu/~history/SUPERCOM.Calle.HTML

    4. +
    +
    +
    + +
    + + + + + \ No newline at end of file diff --git a/site/public/research/index.html b/site/public/research/index.html index cf9546e1..59a5fee9 100644 --- a/site/public/research/index.html +++ b/site/public/research/index.html @@ -28,7 +28,7 @@

    Research Blog

    The darkside of datasets and the future of computer vision

    -
    +
    -- cgit v1.2.3-70-g09d2
    Title