diff options
| author | Adam Harvey <adam@ahprojects.com> | 2018-12-23 01:37:03 +0100 |
|---|---|---|
| committer | Adam Harvey <adam@ahprojects.com> | 2018-12-23 01:37:03 +0100 |
| commit | 4452e02e8b04f3476273574a875bb60cfbb4568b (patch) | |
| tree | 3ffa44f9621b736250a8b94da14a187dc785c2fe /client/faceSearch | |
| parent | 2a65f7a157bd4bace970cef73529867b0e0a374d (diff) | |
| parent | 5340bee951c18910fd764241945f1f136b5a22b4 (diff) | |
.
Diffstat (limited to 'client/faceSearch')
| -rw-r--r-- | client/faceSearch/faceSearch.actions.js | 57 | ||||
| -rw-r--r-- | client/faceSearch/faceSearch.container.js | 24 | ||||
| -rw-r--r-- | client/faceSearch/faceSearch.query.js | 93 | ||||
| -rw-r--r-- | client/faceSearch/faceSearch.reducer.js | 32 | ||||
| -rw-r--r-- | client/faceSearch/faceSearch.result.js | 115 | ||||
| -rw-r--r-- | client/faceSearch/index.js | 5 |
6 files changed, 326 insertions, 0 deletions
diff --git a/client/faceSearch/faceSearch.actions.js b/client/faceSearch/faceSearch.actions.js new file mode 100644 index 00000000..03e1a91d --- /dev/null +++ b/client/faceSearch/faceSearch.actions.js @@ -0,0 +1,57 @@ +// import fetchJsonp from 'fetch-jsonp' +import * as types from '../types' +// import { hashPath } from '../util' +import { store } from '../store' +import { post, preloadImage } 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.faceSearch.loading, + tag, + offset +}) +const loaded = (tag, data, offset = 0) => ({ + type: types.faceSearch.loaded, + tag, + data, + offset +}) +const error = (tag, err) => ({ + type: types.faceSearch.error, + tag, + err +}) + +// search UI functions + +export const updateOptions = opt => dispatch => { + dispatch({ type: types.faceSearch.update_options, opt }) +} + +// API functions + +export const upload = (payload, file) => dispatch => { + // const { options } = store.getState().faceSearch + 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))) +} diff --git a/client/faceSearch/faceSearch.container.js b/client/faceSearch/faceSearch.container.js new file mode 100644 index 00000000..94c6eb9f --- /dev/null +++ b/client/faceSearch/faceSearch.container.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from './faceSearch.actions' + +import FaceSearchQuery from './faceSearch.query' +import FaceSearchResult from './faceSearch.result' + +class FaceSearchContainer extends Component { + render() { + const { payload } = this.props + // console.log(payload) + return ( + <div className='searchContainer'> + <FaceSearchQuery payload={payload} /> + <FaceSearchResult payload={payload} /> + </div> + ) + } +} + + +export default FaceSearchContainer diff --git a/client/faceSearch/faceSearch.query.js b/client/faceSearch/faceSearch.query.js new file mode 100644 index 00000000..425cb282 --- /dev/null +++ b/client/faceSearch/faceSearch.query.js @@ -0,0 +1,93 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import { Loader } from '../common' +import * as actions from './faceSearch.actions' + +class FaceSearchQuery extends Component { + state = { + image: null + } + + upload(e) { + const { payload } = this.props + const files = e.dataTransfer ? e.dataTransfer.files : e.target.files + let i + let file + for (i = 0; i < files.length; i++) { + file = files[i] + if (file && file.type.match('image.*')) break + } + if (!file) return + const fr = new FileReader() + fr.onload = () => { + fr.onload = null + this.setState({ image: fr.result }) + } + fr.readAsDataURL(files[0]) + this.props.actions.upload(this.props.payload, file) + } + + render() { + const { result } = this.props + const { image } = this.state + const style = {} + if (image) { + style.backgroundImage = 'url(' + image + ')' + style.backgroundSize = 'cover' + style.opacity = 1 + } + return ( + <div className='query row'> + <div className='uploadContainer'> + {result.loading ? + <div className='loading' style={style}> + <Loader /> + </div> + : <div style={style}> + {image ? null : <img src="/assets/img/icon_camera.svg" />} + <input + type="file" + name="img" + accept="image/*" + onChange={this.upload.bind(this)} + required + /> + </div> + } + </div> + <div className='cta'> + <h2>Search This Dataset</h2> + <h3>Searching {13456} images</h3> + <p> + {'Use facial recognition to reverse search into the LFW dataset '} + {'and see if it contains your photos.'} + </p> + <ol> + <li>Upload a photo of yourself</li> + <li>Use a photo similar to examples below</li> + <li>Only matches over 85% will be displayed</li> + <li>Read more tips to improve search results</li> + <li>{'Your search data is never stored and immediately cleared '} + {'once you leave this page.'}</li> + </ol> + <p> + Read more about <a href='/about/privacy/'>privacy</a>. + </p> + </div> + </div> + ) + } +} + +const mapStateToProps = state => ({ + result: state.faceSearch.result, + options: state.faceSearch.options, +}) + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(FaceSearchQuery) diff --git a/client/faceSearch/faceSearch.reducer.js b/client/faceSearch/faceSearch.reducer.js new file mode 100644 index 00000000..da8bd25e --- /dev/null +++ b/client/faceSearch/faceSearch.reducer.js @@ -0,0 +1,32 @@ +import * as types from '../types' + +const initialState = () => ({ + query: {}, + result: {}, + loading: false, +}) + +export default function faceSearchReducer(state = initialState(), action) { + switch (action.type) { + case types.faceSearch.loading: + return { + ...state, + [action.tag]: { loading: true }, + } + + case types.faceSearch.loaded: + return { + ...state, + [action.tag]: action.data, + } + + case types.faceSearch.error: + return { + ...state, + [action.tag]: { error: action.err }, + } + + default: + return state + } +} diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js new file mode 100644 index 00000000..936bc8d2 --- /dev/null +++ b/client/faceSearch/faceSearch.result.js @@ -0,0 +1,115 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { courtesyS } from '../util' + +import * as actions from './faceSearch.actions' +import { Loader } from '../common' + +const errors = { + bbox: ( + <div> + <h2>No face found</h2> + {"Sorry, we didn't detect a face in that image. "} + {"Please choose an image where the face is large and clear."} + </div> + ), + nomatch: ( + <div> + <h2>{"You're clear"}</h2> + {"No images in this dataset match your face. We show only matches above 70% probability."} + </div> + ), + error: ( + <div> + <h2>{"No matches found"}</h2> + {"Sorry, an error occured."} + </div> + ), +} + +class FaceSearchResult extends Component { + render() { + const { dataset } = this.props.payload + const { query, distances, results, loading, error } = this.props.result + console.log(this.props.result) + if (loading) { + return ( + <div className='result'> + <div> + <Loader /><br /> + <h2>Searching...</h2> + </div> + </div> + ) + } + if (error) { + // console.log(error) + let errorMessage = errors[error] || errors.error + return ( + <div className='result'>{errorMessage}</div> + ) + } + if (!results) { + return <div className='result'></div> + } + if (!results.length) { + return ( + <div className='result'>{errors.nomatch}</div> + ) + } + const els = results.map((result, i) => { + const distance = distances[i] + const { uuid } = result.uuid + const { x, y, w, h } = result.roi + const { fullname, gender, description, images } = result.identity + const bbox = { + left: (100 * x) + '%', + top: (100 * y) + '%', + width: (100 * w) + '%', + height: (100 * h) + '%', + } + // console.log(bbox) + return ( + <div key={i}> + <div className='img'> + <img src={'https://megapixels.nyc3.digitaloceanspaces.com/v1/media/' + dataset + '/' + uuid + '.jpg'} /> + <div className='bbox' style={bbox} /> + </div> + {fullname} {'('}{gender}{')'}<br/> + {description}<br/> + {courtesyS(images, 'image')}{' in dataset'}<br /> + {Math.round((1 - distance) * 100)}{'% match'} + </div> + ) + }) + + return ( + <div className='result'> + <div className="about"> + <h2>Did we find you?</h2> + {'These faces matched images in the '} + <b><tt>{dataset}</tt></b> + {' dataset with over 70% probability.'} + <br /> + <small>Query took {query.timing.toFixed(2)} seconds</small> + </div> + <div className='results'> + {els} + </div> + </div> + ) + } +} + +const mapStateToProps = state => ({ + query: state.faceSearch.query, + result: state.faceSearch.result, + options: state.faceSearch.options, +}) + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(FaceSearchResult) diff --git a/client/faceSearch/index.js b/client/faceSearch/index.js new file mode 100644 index 00000000..32f6dcc6 --- /dev/null +++ b/client/faceSearch/index.js @@ -0,0 +1,5 @@ +import Container from './faceSearch.container' + +export { + Container, +} |
