From 276c16e1055c23350abd3d9d071cfce9b4f1b27f Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Fri, 4 Jan 2019 23:37:34 +0100 Subject: rm rotation on homepage, add dataset list --- README.md | 1 + client/faceSearch/faceSearch.query.js | 2 +- client/faceSearch/faceSearch.result.js | 12 +++ client/faceSearch/upload.js | 154 +++++++++++++++++++++++++++++++++ client/nameSearch/nameSearch.query.js | 8 +- client/tables.js | 6 +- megapixels/app/models/sql_factory.py | 3 +- megapixels/app/server/api.py | 8 +- megapixels/app/site/builder.py | 20 +++-- megapixels/app/site/parser.py | 16 +++- site/assets/css/applets.css | 24 ++++- site/assets/css/css.css | 68 ++++++++++++--- site/assets/js/app/face.js | 6 +- site/public/datasets/index.html | 25 ++++++ site/public/datasets/lfw/index.html | 6 +- site/public/index.html | 25 +++++- site/templates/datasets.html | 24 +++++ site/templates/home.html | 17 ++++ 18 files changed, 384 insertions(+), 41 deletions(-) create mode 100644 client/faceSearch/upload.js create mode 100644 site/templates/datasets.html diff --git a/README.md b/README.md index ed692e40..aa6e3335 100644 --- a/README.md +++ b/README.md @@ -41,5 +41,6 @@ cd megapixels python cli_faiss.py sync_metadata python cli_faiss.py build_faiss python cli_faiss.py build_db +python cli_site.py build python cli_flask.py run ``` diff --git a/client/faceSearch/faceSearch.query.js b/client/faceSearch/faceSearch.query.js index 20d200bb..9f778ca0 100644 --- a/client/faceSearch/faceSearch.query.js +++ b/client/faceSearch/faceSearch.query.js @@ -68,7 +68,7 @@ class FaceSearchQuery extends Component { }
-

Search This Dataset

+

Search by Image

Searching {13456} images

{'Use facial recognition to reverse search into the LFW dataset '} diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index 936bc8d2..95534830 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -26,6 +26,18 @@ const errors = { {"Sorry, an error occured."}

), + bad_dataset: ( +
+

{""}

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

{"Not an image"}

+ {"Sorry, the file you uploaded was not recognized as an image. Please upload a JPG or PNG image and try again."} +
+ ), } class FaceSearchResult extends Component { diff --git a/client/faceSearch/upload.js b/client/faceSearch/upload.js new file mode 100644 index 00000000..f18bdce6 --- /dev/null +++ b/client/faceSearch/upload.js @@ -0,0 +1,154 @@ +const MAX_SIDE = 300 + +function render(img){ + var resized = renderToCanvas(img, { correctOrientation: true }) + var canvas = document.createElement('canvas') // document.querySelector('#user_photo_canvas') + ctx = canvas.getContext('2d') + ctx.fillStyle = 'black' + ctx.fillRect(0, 0, MAX_SIDE, MAX_SIDE) + var x_offset = (MAX_SIDE - resized.width) / 2 + var y_offset = (MAX_SIDE - resized.height) / 2 + ctx.drawImage(resized, x_offset, y_offset) + return canvas +} +function renderToCanvas(img, options) { + if (!img) return + options = options || {} + + // Canvas max size for any side + var maxSize = MAX_SIDE + var canvas = document.createElement('canvas') + var ctx = canvas.getContext('2d') + var initialScale = options.scale || 1 + // Scale to needed to constrain canvas to max size + var scale = getScale(img.width * initialScale, img.height * initialScale, maxSize, maxSize, true) + // Still need to apply the user defined scale + scale *= initialScale + var width = canvas.width = Math.round(img.width * scale) + var height = canvas.height = Math.round(img.height * scale) + var correctOrientation = options.correctOrientation + var jpeg = !!img.src.match(/data:image\/jpeg|\.jpeg$|\.jpg$/i) + var hasDataURI = !!img.src.match(/^data:/) + + ctx.save() + + // Can only correct orientation on JPEGs represented as dataURIs + // for the time being + if (correctOrientation && jpeg && hasDataURI) { + applyOrientationCorrection(canvas, ctx, img.src) + } + // Resize image if too large + if (scale !== 1) { + ctx.scale(scale, scale) + } + + ctx.drawImage(img, 0, 0) + ctx.restore() + + return canvas +} + +function getScale(width, height, viewportWidth, viewportHeight, fillViewport) { + fillViewport = !!fillViewport + var landscape = (width / height) > (viewportWidth / viewportHeight) + if (landscape) { + if (fillViewport) { + return fitVertical() + } else if (width > viewportWidth) { + return fitHorizontal() + } + } else { + if (fillViewport) { + return fitHorizontal() + } else if (height > viewportHeight) { + return fitVertical() + } + } + return 1 + + function fitHorizontal() { + return viewportWidth / width + } + + function fitVertical() { + return viewportHeight / height + } +} + +function applyOrientationCorrection(canvas, ctx, uri) { + var orientation = getOrientation(uri) + // Only apply transform if there is some non-normal orientation + if (orientation && orientation !== 1) { + var transform = orientationToTransform[orientation] + var rotation = transform.rotation + var mirror = transform.mirror + var flipAspect = rotation === 90 || rotation === 270 + if (flipAspect) { + // Fancy schmancy swap algo + canvas.width = canvas.height + canvas.width + canvas.height = canvas.width - canvas.height + canvas.width -= canvas.height + } + if (rotation > 0) { + applyRotation(canvas, ctx, rotation) + } + } +} + +function applyRotation(canvas, ctx, deg) { + var radians = deg * (Math.PI / 180) + if (deg === 90) { + ctx.translate(canvas.width, 0) + } else if (deg === 180) { + ctx.translate(canvas.width, canvas.height) + } else if (deg == 270) { + ctx.translate(0, canvas.height) + } + ctx.rotate(radians) +} + +function getOrientation (uri) { + var exif = new ExifReader + // Split off the base64 data + var base64String = uri.split(',')[1] + // Read off first 128KB, which is all we need to + // get the EXIF data + var arr = base64ToUint8Array(base64String, 0, Math.pow(2, 17)) + try { + exif.load(arr.buffer) + return exif.getTagValue('Orientation') + } catch (err) { + return 1 + } +} + +function base64ToUint8Array(string, start, finish) { + var start = start || 0 + var finish = finish || string.length + // atob that shit + var binary = atob(string) + var buffer = new Uint8Array(binary.length) + for (var i = start; i < finish; i++) { + buffer[i] = binary.charCodeAt(i) + } + return buffer +} + +/** + * Mapping from EXIF orientation values to data + * regarding the rotation and mirroring necessary to + * render the canvas correctly + * Derived from: + * http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ + */ +var orientationToTransform = { + 1: { rotation: 0, mirror: false }, + 2: { rotation: 0, mirror: true }, + 3: { rotation: 180, mirror: false }, + 4: { rotation: 180, mirror: true }, + 5: { rotation: 90, mirror: true }, + 6: { rotation: 90, mirror: false }, + 7: { rotation: 270, mirror: true }, + 8: { rotation: 270, mirror: false } +} + diff --git a/client/nameSearch/nameSearch.query.js b/client/nameSearch/nameSearch.query.js index b82e324b..629b7b1d 100644 --- a/client/nameSearch/nameSearch.query.js +++ b/client/nameSearch/nameSearch.query.js @@ -11,22 +11,22 @@ class NameSearchQuery extends Component { handleInput(value) { this.setState({ q: value }) - if (value.length > 2) { - this.props.actions.search(this.props.payload, value) + if (value.strip().length > 1) { + this.props.actions.search(this.props.payload, value.strip()) } } render() { return (
-

Find Your Name

+

Search by Name

Searching {13456} identities

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

this.handleInput(e.target.value)} diff --git a/client/tables.js b/client/tables.js index b4c13887..9e134eb6 100644 --- a/client/tables.js +++ b/client/tables.js @@ -75,7 +75,7 @@ export default function append(el, payload) { }) } - if (fields.length > 1 && fields[1].indexOf('filter')) { - const filter = fields[1].split(' ') - } + // if (fields && fields.length > 1 && fields[1].indexOf('filter')) { + // const filter = fields[1].split(' ') + // } } diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index b270afd2..a580f28e 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -61,7 +61,8 @@ def load_sql_dataset(path, replace=False, engine=None, base_model=None): print('loading dataset {}'.format(fn)) df = pd.read_csv(fn) # fix columns that are named "index", a sql reserved word - df.columns = table.__table__.columns.keys() + df.reindex_axis(sorted(df.columns), axis=1) + df.columns = sorted(table.__table__.columns).keys() df.to_sql(name=table.__tablename__, con=engine, if_exists='replace', index=False) return dataset diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 35862837..3683d5fd 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -45,18 +45,20 @@ def upload(dataset_name): dataset = get_dataset(dataset_name) if dataset_name not in faiss_datasets: return jsonify({ - 'error': 'invalid dataset' + 'error': 'bad_dataset' }) faiss_dataset = faiss_datasets[dataset_name] file = request.files['query_img'] fn = file.filename - if fn.endswith('blob'): + if fn.endswith('blob'): # FIX PNG IMAGES? 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' }) + return jsonify({ + 'error': 'not_an_image' + }) im = Image.open(file.stream).convert('RGB') im_np = pil2np(im) diff --git a/megapixels/app/site/builder.py b/megapixels/app/site/builder.py index ff1a0c83..fac49c24 100644 --- a/megapixels/app/site/builder.py +++ b/megapixels/app/site/builder.py @@ -14,7 +14,7 @@ env = Environment( autoescape=select_autoescape([]) ) -def build_page(fn, research_posts): +def build_page(fn, research_posts, datasets): """ build a single page from markdown into the appropriate template - writes it to site/public/ @@ -40,6 +40,8 @@ def build_page(fn, research_posts): elif 'research/' in fn: skip_h1 = True template = env.get_template("research.html") + elif 'datasets/index' in fn: + template = env.get_template("datasets.html") else: template = env.get_template("page.html") @@ -60,17 +62,18 @@ def build_page(fn, research_posts): content=content, research_posts=research_posts, latest_research_post=research_posts[-1], + datasets=datasets, ) os.makedirs(output_path, exist_ok=True) with open(output_fn, "w") as file: file.write(html) -def build_research_index(research_posts): +def build_index(key, research_posts, datasets): """ build the index of research (blog) posts """ - metadata, sections = parser.read_metadata('../site/content/research/index.md') + metadata, sections = parser.read_metadata('../site/content/{}/index.md'.format(key)) template = env.get_template("page.html") s3_path = s3.make_s3_path(cfg.S3_SITE_PATH, metadata['path']) content = parser.parse_markdown(sections, s3_path, skip_h1=False) @@ -80,8 +83,9 @@ def build_research_index(research_posts): content=content, research_posts=research_posts, latest_research_post=research_posts[-1], + datasets=datasets, ) - output_fn = cfg.DIR_SITE_PUBLIC + '/research/index.html' + output_fn = '{}/{}/index.html'.format(cfg.DIR_SITE_PUBLIC, key) with open(output_fn, "w") as file: file.write(html) @@ -90,14 +94,16 @@ def build_site(): build the site! =^) """ research_posts = parser.read_research_post_index() + datasets = parser.read_datasets_index() for fn in glob.iglob(os.path.join(cfg.DIR_SITE_CONTENT, "**/*.md"), recursive=True): - build_page(fn, research_posts) - build_research_index(research_posts) + build_page(fn, research_posts, datasets) + build_index('research', research_posts, datasets) def build_file(fn): """ build just one page from a filename! =^) """ research_posts = parser.read_research_post_index() + datasets = parser.read_datasets_index() fn = os.path.join(cfg.DIR_SITE_CONTENT, fn) - build_page(fn, research_posts) + build_page(fn, research_posts, datasets) diff --git a/megapixels/app/site/parser.py b/megapixels/app/site/parser.py index b3d3a8c2..d3eccfca 100644 --- a/megapixels/app/site/parser.py +++ b/megapixels/app/site/parser.py @@ -66,6 +66,8 @@ def format_applet(section, s3_path): opt = None if command == 'python' or command == 'javascript' or command == 'code': return format_section([ section ], s3_path) + if command == '': + return '' applet['command'] = command if opt: @@ -221,8 +223,20 @@ def read_research_post_index(): """ Generate an index of the research (blog) posts """ + return read_post_index('research') + +def read_datasets_index(): + """ + Generate an index of the datasets + """ + return read_post_index('datasets') + +def read_post_index(basedir): + """ + Generate an index of posts + """ posts = [] - for fn in sorted(glob.glob('../site/content/research/*/index.md')): + for fn in sorted(glob.glob('../site/content/{}/*/index.md'.format(basedir))): metadata, valid_sections = read_metadata(fn) if metadata is None or metadata['status'] == 'private' or metadata['status'] == 'draft': continue diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index 315d72e0..9c37354a 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -65,12 +65,17 @@ } .cta { padding-left: 20px; - font-size: 11pt; + font-size: 10pt; } .cta ol { margin: 0; padding: 0 0 20px 20px; } + +.searchContainer { + padding-top: 20px; +} + .uploadContainer > div { position: relative; width: 300px; @@ -98,3 +103,20 @@ .uploadContainer img { max-width: 40px; } + + +/* tabulator */ + +.tabulator-row { + transition: background-color 100ms cubic-bezier(0,0,1,1); + background-color: rgba(255,255,255,0.0); +} +.desktop .tabulator-row:hover { + background-color: rgba(255,255,255,0.2); +} +.tabulator-row.tabulator-row-odd { + background-color: rgba(255,255,255,0.05); +} +.tabulator-row.tabulator-row-even { + background-color: rgba(255,255,255,0.1); +} \ No newline at end of file diff --git a/site/assets/css/css.css b/site/assets/css/css.css index 003ac4a3..7e354a4c 100644 --- a/site/assets/css/css.css +++ b/site/assets/css/css.css @@ -1,4 +1,4 @@ -* { box-sizing: border-box; } +* { box-sizing: border-box; outline: 0; } html, body { margin: 0; padding: 0; @@ -27,7 +27,7 @@ header { left: 0; width: 100%; height: 70px; - z-index: 1; + z-index: 2; background: #1e1e1e; display: flex; flex-direction: row; @@ -44,11 +44,11 @@ header .slogan { } header .logo { background-image: url(../img/megapixels_logo_white.svg); - background-size: cover; + background-size: contain; background-repeat: no-repeat; margin-top: 7px; - margin-right: 14px; - width: 49px; + margin-right: 10px; + width: 39px; height: 30px; } header .site_name { @@ -175,13 +175,16 @@ th, .gray { line-height: 1.5; } section { - width: 640px; + width: 960px; margin: 0 auto; } .content .first_paragraph { font-weight: 300; - font-size: 18pt; - color: #ccc; + font-size: 16pt; + color: #ddd; + margin-bottom: 50px; + margin-top: 30px; + line-height: 36px; } p { margin: 0 0 20px 0; @@ -212,6 +215,16 @@ p { padding-bottom: 4px; } +/* lists */ + +ul { + list-style-type: none; + margin: 0 0 30px 0; + padding: 0; +} +ul li { + margin-bottom: 8px; +} /* misc formatting */ code { @@ -368,18 +381,19 @@ section.wide .image { } .intro .headline { font-family: 'Roboto Mono', monospace; - font-size: 16pt; + font-size: 28pt; + line-height: 40pt; } .intro .buttons { margin: 40px 0; } .intro button { font-family: 'Roboto', sans-serif; - padding: 8px 12px; - border-radius: 6px; + padding: 15px 20px; + border-radius: 8px; border: 1px solid transparent; cursor: pointer; - font-size: 11pt; + font-size: 12pt; margin-right: 10px; transition: color 0.1s cubic-bezier(0,0,1,1), background-color 0.1s cubic-bezier(0,0,1,1); } @@ -406,4 +420,32 @@ section.wide .image { } .desktop .intro .under a:hover { color: #fff; -} \ No newline at end of file +} + +/* intro - list of datasets */ + +.dataset-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} +.dataset-list a { + text-decoration: none; + transition: background-color 0.1s cubic-bezier(0,0,1,1); + background: black; + margin: 0 20px 20px 0; +} +.dataset-list .dataset { + width: 220px; + height: 140px; + padding: 10px; + color: white; +} +.dataset-list a:nth-child(3n+1) { background-color: rgba(255, 0, 0, 0.1); } +.desktop .dataset-list a:nth-child(3n+1):hover { background-color: rgba(255, 0, 0, 0.2); } + +.dataset-list a:nth-child(3n+2) { background-color: rgba(255, 128, 0, 0.1); } +.desktop .dataset-list a:nth-child(3n+2):hover { background-color: rgba(255, 128, 0, 0.2); } + +.dataset-list .dataset:nth-child(3n+3) { background-color: rgba(255, 255, 0, 0.1); } +.desktop .dataset-list .dataset:nth-child(3n+3):hover { background-color: rgba(255, 255, 0, 0.2); } \ No newline at end of file diff --git a/site/assets/js/app/face.js b/site/assets/js/app/face.js index bdaa0313..f3f1f2bf 100644 --- a/site/assets/js/app/face.js +++ b/site/assets/js/app/face.js @@ -216,12 +216,12 @@ var face = (function(){ requestAnimationFrame(animate) if (swapping) update_swap(t) renderer.render(scene, camera) - scene.rotation.y += 0.01 * Math.PI + // scene.rotation.y += 0.01 * Math.PI mouseTarget.x += (mouse.x - mouseTarget.x) * 0.1 mouseTarget.y += (mouse.y - mouseTarget.y) * 0.1 scene.rotation.x = (mouseTarget.y - 0.5) * Math.PI / 2 - // scene.rotation.y = (mouseTarget.x - 0.5) * Math.PI - scene.rotation.y += 0.01 + scene.rotation.y = (mouseTarget.x - 0.5) * Math.PI + // scene.rotation.y += 0.01 last_t = t } })() diff --git a/site/public/datasets/index.html b/site/public/datasets/index.html index bcc7c1ab..4d6f57b6 100644 --- a/site/public/datasets/index.html +++ b/site/public/datasets/index.html @@ -28,11 +28,36 @@
+

Facial Recognition Datasets

Regular Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Summary

Found
275 datasets
Created between
1993-2018
Smallest dataset
20 images
Largest dataset
10,000,000 images
Highest resolution faces
450x500 (Unconstrained College Students)
Lowest resolution faces
16x20 pixels (QMUL SurvFace)
+
+

Dataset Portraits

+

+ We have prepared detailed studies of some of the more noteworthy datasets. +

+ + +
+ +