From bb7efd0af0db8183b5b3f96ac0de1bfd9cd249ae Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 13 Jan 2019 00:04:35 +0100 Subject: face analysis frontend scaffolding --- client/faceAnalysis/faceAnalysis.actions.js | 75 +++++++++++++++ client/faceAnalysis/faceAnalysis.container.js | 24 +++++ client/faceAnalysis/faceAnalysis.query.js | 82 +++++++++++++++++ client/faceAnalysis/faceAnalysis.reducer.js | 32 +++++++ client/faceAnalysis/faceAnalysis.result.js | 127 ++++++++++++++++++++++++++ client/faceAnalysis/index.js | 5 + 6 files changed, 345 insertions(+) create mode 100644 client/faceAnalysis/faceAnalysis.actions.js create mode 100644 client/faceAnalysis/faceAnalysis.container.js create mode 100644 client/faceAnalysis/faceAnalysis.query.js create mode 100644 client/faceAnalysis/faceAnalysis.reducer.js create mode 100644 client/faceAnalysis/faceAnalysis.result.js create mode 100644 client/faceAnalysis/index.js (limited to 'client/faceAnalysis') diff --git a/client/faceAnalysis/faceAnalysis.actions.js b/client/faceAnalysis/faceAnalysis.actions.js new file mode 100644 index 00000000..90d7156f --- /dev/null +++ b/client/faceAnalysis/faceAnalysis.actions.js @@ -0,0 +1,75 @@ +// import fetchJsonp from 'fetch-jsonp' +import * as types from '../types' +// import { hashPath } from '../util' +import { store } from '../store' +import { get, post } from '../util' +// import querystring from 'query-string' + +// urls + +const url = { + upload: (dataset) => process.env.API_HOST + '/api/dataset/' + dataset + '/face', +} +export const publicUrl = { +} + +// standard loading events + +const loading = (tag, offset) => ({ + type: types.faceAnalysis.loading, + tag, + offset +}) +const loaded = (tag, data, offset = 0) => ({ + type: types.faceAnalysis.loaded, + tag, + data, + offset +}) +const error = (tag, err) => ({ + type: types.faceAnalysis.error, + tag, + err +}) + +// search UI functions + +export const updateOptions = opt => dispatch => { + dispatch({ type: types.faceAnalysis.update_options, opt }) +} + +// API functions + +export const upload = (payload, file) => dispatch => { + // const { options } = store.getState().faceAnalysis + const tag = 'result' + const fd = new FormData() + fd.append('query_img', file) + // fd.append('limit', options.perPage) + // if (!query) { + dispatch(loading(tag)) + // } + post(url.upload(payload.dataset), fd) + .then(data => { + dispatch(loaded(tag, data)) + }) + .catch(err => dispatch(error(tag, err))) +} + +// task polling + +const POLL_DELAY = 500 +let pollTimeout = null + +export const poll = (payload, taskURL) => dispatch => { + const tag = 'poll' + clearTimeout(pollTimeout) + dispatch(loading(tag)) + get(taskURL) + .then(data => { + dispatch(loaded(tag, data)) + // check if complete + pollTimeout = setTimeout(() => poll(payload, taskURL), POLL_DELAY) + }) + .catch(err => dispatch(error(tag, err))) +} diff --git a/client/faceAnalysis/faceAnalysis.container.js b/client/faceAnalysis/faceAnalysis.container.js new file mode 100644 index 00000000..a86bcaa4 --- /dev/null +++ b/client/faceAnalysis/faceAnalysis.container.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from './faceAnalysis.actions' + +import FaceAnalysisQuery from './faceAnalysis.query' +import FaceAnalysisResult from './faceAnalysis.result' + +class FaceAnalysisContainer extends Component { + render() { + const { payload } = this.props + // console.log(payload) + return ( +
+ + +
+ ) + } +} + + +export default FaceAnalysisContainer diff --git a/client/faceAnalysis/faceAnalysis.query.js b/client/faceAnalysis/faceAnalysis.query.js new file mode 100644 index 00000000..86dbe1ae --- /dev/null +++ b/client/faceAnalysis/faceAnalysis.query.js @@ -0,0 +1,82 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import { Loader } from '../common' +import * as actions from './faceAnalysis.actions' + +// function parse_bbox(s) { +// // "BBox: (77,86), (166, 177), width:89, height:91" +// try { +// const [x, y, w, h, width, height] = s.replace(/\D/g, ' ').replace(/\s+/g, ' ').trim().split(' ') +// return { x, y, w, h } +// } +// } + +class FaceAnalysisQuery extends Component { + state = { + image: null + } + + upload(blob) { + this.props.actions.upload(this.props.payload, blob) + } + + render() { + const { result } = this.props + const { image } = this.state + console.log(result) + const style = {} + if (image) { + style.backgroundImage = 'url(' + image + ')' + style.backgroundSize = 'cover' + style.opacity = 1 + } + return ( +
+
+
+ {image ? null : } + +
+ {result.loading && ( +
+ +
+ )} +
+
+

Face Analysis

+

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

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

+ Read more about privacy. +

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

No face found

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

{"You're clear"}

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

{"No matches found"}

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

{""}

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

{"Not an image"}

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

Searching...

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

Did we find you?

+ {'These faces matched images in the '} + {dataset} + {' dataset with over 70% probability.'} +
+ Query took {query.timing.toFixed(2)} seconds +
+
+ {els} +
+
+ ) + } +} + +const mapStateToProps = state => ({ + query: state.faceAnalysis.query, + result: state.faceAnalysis.result, + options: state.faceAnalysis.options, +}) + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(FaceAnalysisResult) diff --git a/client/faceAnalysis/index.js b/client/faceAnalysis/index.js new file mode 100644 index 00000000..efa39ded --- /dev/null +++ b/client/faceAnalysis/index.js @@ -0,0 +1,5 @@ +import Container from './faceAnalysis.container' + +export { + Container, +} -- cgit v1.2.3-70-g09d2