summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
authoradamhrv <adam@ahprojects.com>2019-01-14 22:25:25 +0100
committeradamhrv <adam@ahprojects.com>2019-01-14 22:25:25 +0100
commitdf9d364e3664f45c65cac5990d3d742b990217fa (patch)
tree8842d844a5ea8e6c87599b8683009cba23262713 /client
parent2fedd95fcee3f048c5f24333ffdb9bb4e13eafe2 (diff)
parent3b2f0dc6d969fa323fe8775b4269e17c60192431 (diff)
Merge branch 'master' of github.com:adamhrv/megapixels_dev
Diffstat (limited to 'client')
-rw-r--r--client/actions.js2
-rw-r--r--client/applet.js3
-rw-r--r--client/common/index.js2
-rw-r--r--client/common/upload.helpers.js (renamed from client/faceSearch/upload.js)240
-rw-r--r--client/common/uploadImage.component.js47
-rw-r--r--client/faceAnalysis/faceAnalysis.actions.js91
-rw-r--r--client/faceAnalysis/faceAnalysis.container.js24
-rw-r--r--client/faceAnalysis/faceAnalysis.query.js86
-rw-r--r--client/faceAnalysis/faceAnalysis.reducer.js48
-rw-r--r--client/faceAnalysis/faceAnalysis.result.js131
-rw-r--r--client/faceAnalysis/index.js5
-rw-r--r--client/faceSearch/faceSearch.query.js42
-rw-r--r--client/faceSearch/faceSearch.result.js4
-rw-r--r--client/index.js2
-rw-r--r--client/nameSearch/nameSearch.query.js4
-rw-r--r--client/nameSearch/nameSearch.result.js2
-rw-r--r--client/store.js2
-rw-r--r--client/types.js4
-rw-r--r--client/util.js17
19 files changed, 599 insertions, 157 deletions
diff --git a/client/actions.js b/client/actions.js
index 2be8229d..7007eb76 100644
--- a/client/actions.js
+++ b/client/actions.js
@@ -1,7 +1,9 @@
+import * as faceAnalysis from './faceAnalysis/faceAnalysis.actions'
import * as faceSearch from './faceSearch/faceSearch.actions'
import * as nameSearch from './nameSearch/nameSearch.actions'
export {
+ faceAnalysis,
faceSearch,
nameSearch,
}
diff --git a/client/applet.js b/client/applet.js
index 80d40657..25291401 100644
--- a/client/applet.js
+++ b/client/applet.js
@@ -1,12 +1,15 @@
import React, { Component } from 'react'
import { Container as FaceSearchContainer } from './faceSearch'
+import { Container as FaceAnalysisContainer } from './faceAnalysis'
import { Container as NameSearchContainer } from './nameSearch'
export default class Applet extends Component {
render() {
// console.log(this.props)
switch (this.props.payload.cmd) {
+ case 'face_analysis':
+ return <FaceAnalysisContainer {...this.props} />
case 'face_search':
return <FaceSearchContainer {...this.props} />
case 'name_search':
diff --git a/client/common/index.js b/client/common/index.js
index cfb34b32..cbd3166e 100644
--- a/client/common/index.js
+++ b/client/common/index.js
@@ -3,6 +3,7 @@ import DetectionBoxes from './detectionBoxes.component'
import DetectionList from './detectionList.component'
// import Header from './header.component'
import Loader from './loader.component'
+import UploadImage from './uploadImage.component'
import Sidebar from './sidebar.component'
import Gate from './gate.component'
import Video from './video.component'
@@ -12,6 +13,7 @@ import './common.css'
export {
Sidebar,
Loader,
+ UploadImage,
Gate,
TableObject,
TableArray,
diff --git a/client/faceSearch/upload.js b/client/common/upload.helpers.js
index f18bdce6..4b38fb09 100644
--- a/client/faceSearch/upload.js
+++ b/client/common/upload.helpers.js
@@ -1,139 +1,46 @@
-const MAX_SIDE = 300
+import ExifReader from 'exifreader'
-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
-}
+export const MAX_SIDE = 256
-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 base64ToUint8Array(string, start, finish) {
+ start = start || 0
+ finish = finish || string.length
+ // atob that shit
+ const binary = atob(string)
+ const buffer = new Uint8Array(binary.length)
+ for (let i = start; i < finish; i++) {
+ buffer[i] = binary.charCodeAt(i)
}
+ return buffer
}
-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 getOrientation(uri) {
+ // Split off the base64 data
+ const base64String = uri.split(',')[1]
+ // Read off first 128KB, which is all we need to
+ // get the EXIF data
+ const arr = base64ToUint8Array(base64String, 0, 2 ** 17)
+ try {
+ const tags = ExifReader.load(arr.buffer)
+ // console.log(tags)
+ return tags.Orientation
+ } catch (err) {
+ return 1
}
}
function applyRotation(canvas, ctx, deg) {
- var radians = deg * (Math.PI / 180)
+ const 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) {
+ } 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
@@ -141,7 +48,7 @@ function base64ToUint8Array(string, start, finish) {
* Derived from:
* http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
*/
-var orientationToTransform = {
+const orientationToTransform = {
1: { rotation: 0, mirror: false },
2: { rotation: 0, mirror: true },
3: { rotation: 180, mirror: false },
@@ -152,3 +59,98 @@ var orientationToTransform = {
8: { rotation: 270, mirror: false }
}
+function applyOrientationCorrection(canvas, ctx, uri) {
+ const orientation = getOrientation(uri)
+ // Only apply transform if there is some non-normal orientation
+ if (orientation && orientation !== 1) {
+ const transform = orientationToTransform[orientation]
+ const { rotation } = transform
+ const 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 getScale(width, height, viewportWidth, viewportHeight, fillViewport) {
+ function fitHorizontal() {
+ return viewportWidth / width
+ }
+ function fitVertical() {
+ return viewportHeight / height
+ }
+ fillViewport = !!fillViewport
+ const landscape = (width / height) > (viewportWidth / viewportHeight)
+ if (landscape) {
+ if (fillViewport) {
+ return fitVertical()
+ }
+ if (width > viewportWidth) {
+ return fitHorizontal()
+ }
+ } else {
+ if (fillViewport) {
+ return fitHorizontal()
+ }
+ if (height > viewportHeight) {
+ return fitVertical()
+ }
+ }
+ return 1
+}
+
+export function renderToCanvas(img, options) {
+ if (!img) return null
+ options = options || {}
+
+ // Canvas max size for any side
+ const maxSide = MAX_SIDE
+ const canvas = document.createElement('canvas')
+ const ctx = canvas.getContext('2d')
+ const initialScale = options.scale || 1
+ // Scale to needed to constrain canvas to max size
+ let scale = getScale(img.naturalWidth * initialScale, img.naturalHeight * initialScale, maxSide, maxSide, true)
+ console.log(scale)
+ // Still need to apply the user defined scale
+ scale *= initialScale
+ canvas.width = Math.round(img.naturalWidth * scale)
+ canvas.height = Math.round(img.naturalHeight * scale)
+ const { correctOrientation } = options
+ const jpeg = !!img.src.match(/data:image\/jpeg|\.jpeg$|\.jpg$/i)
+ const 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
+}
+
+export function renderThumbnail(img) {
+ const resized = renderToCanvas(img, { correctOrientation: true })
+ // const canvas = document.createElement('canvas') // document.querySelector('#user_photo_canvas')
+ // const ctx = canvas.getContext('2d')
+ // ctx.fillStyle = 'black'
+ // ctx.fillRect(0, 0, MAX_SIDE, MAX_SIDE)
+ // const xOffset = (MAX_SIDE - resized.width) / 2
+ // const yOffset = (MAX_SIDE - resized.height) / 2
+ // ctx.drawImage(resized, xOffset, yOffset, resized.width, resized.height)
+ return resized
+}
diff --git a/client/common/uploadImage.component.js b/client/common/uploadImage.component.js
new file mode 100644
index 00000000..bc88828e
--- /dev/null
+++ b/client/common/uploadImage.component.js
@@ -0,0 +1,47 @@
+import React, { Component } from 'react'
+
+import { renderThumbnail } from './upload.helpers'
+
+export default class UploadImageComponent extends Component {
+ upload(e) {
+ 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 = fileReaderEvent => {
+ fr.onload = null
+ const img = new Image()
+ img.onload = () => {
+ img.onload = null
+ this.resizeAndUpload(img)
+ }
+ img.src = fileReaderEvent.target.result
+ }
+ fr.readAsDataURL(files[0])
+ }
+
+ resizeAndUpload(img) {
+ const canvas = renderThumbnail(img)
+ canvas.toBlob(blob => {
+ // console.log(blob)
+ this.props.onUpload(blob)
+ }, 'image/jpeg', 80)
+ }
+
+ render() {
+ return (
+ <input
+ type="file"
+ name="img"
+ accept="image/*"
+ onChange={this.upload.bind(this)}
+ required
+ />
+ )
+ }
+}
diff --git a/client/faceAnalysis/faceAnalysis.actions.js b/client/faceAnalysis/faceAnalysis.actions.js
new file mode 100644
index 00000000..4a6fe6ed
--- /dev/null
+++ b/client/faceAnalysis/faceAnalysis.actions.js
@@ -0,0 +1,91 @@
+// 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: () => process.env.API_HOST + '/task/upload/demo',
+}
+export const publicUrl = {
+}
+
+// standard loading events
+
+const loading = (tag, offset) => ({
+ ts: Date.now(),
+ type: types.faceAnalysis.loading,
+ tag,
+ offset
+})
+const loaded = (tag, data, offset = 0) => ({
+ ts: Date.now(),
+ type: types.faceAnalysis.loaded,
+ tag,
+ data,
+ offset
+})
+const polled = (data, offset = 0) => ({
+ ts: Date.now(),
+ type: types.faceAnalysis.poll,
+ 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
+
+// task polling
+
+const POLL_DELAY = 500
+let pollTimeout = null
+
+export const poll = (payload, taskURL) => dispatch => {
+ clearTimeout(pollTimeout)
+ // console.log('polling...')
+ get(taskURL)
+ .then(data => {
+ // console.log('poll', data)
+ dispatch(polled(data))
+ // console.log(data.state)
+ if (data.state === 'COMPLETE' || data.state === 'SUCCESS') {
+ console.log('complete!')
+ } else if (data.state === 'ERROR' || data.state === 'FAILURE') {
+ console.log('errorr!')
+ dispatch(error(data))
+ } else {
+ pollTimeout = setTimeout(() => poll(payload, taskURL)(dispatch), POLL_DELAY)
+ }
+ })
+ .catch(err => dispatch(error('result', err)))
+}
+
+export const upload = (payload, file) => dispatch => {
+ const tag = 'task'
+ const fd = new FormData()
+ fd.append('query_img', file)
+ dispatch(loading(tag))
+ post(url.upload(), fd)
+ .then(data => {
+ // console.log('loaded!', tag, data)
+ dispatch(loaded(tag, data))
+ const { result, taskURL } = data
+ if (result && taskURL) {
+ poll(payload, taskURL)(dispatch)
+ }
+ })
+ .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..24848455
--- /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 (
+ <div className='analysisContainer'>
+ <FaceAnalysisQuery payload={payload} />
+ <FaceAnalysisResult payload={payload} />
+ </div>
+ )
+ }
+}
+
+
+export default FaceAnalysisContainer
diff --git a/client/faceAnalysis/faceAnalysis.query.js b/client/faceAnalysis/faceAnalysis.query.js
new file mode 100644
index 00000000..33dd641f
--- /dev/null
+++ b/client/faceAnalysis/faceAnalysis.query.js
@@ -0,0 +1,86 @@
+import React, { Component } from 'react'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import { Loader, UploadImage } 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) {
+ if (this.state.image) {
+ URL.revokeObjectURL(this.state.image)
+ }
+ const url = URL.createObjectURL(blob)
+ this.setState({ image: url })
+ this.props.actions.upload(this.props.payload, blob)
+ }
+
+ componentWillUnmount() {
+ if (this.state.image) {
+ URL.revokeObjectURL(this.state.image)
+ }
+ }
+
+ 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'>
+ <div style={style}>
+ {image ? null : <img src="/assets/img/icon_camera.svg" />}
+ <UploadImage onUpload={this.upload.bind(this)} />
+ </div>
+ {result.loading && (
+ <div className='loading' style={style}>
+ <Loader />
+ </div>
+ )}
+ </div>
+ <div className='cta'>
+ <h2>Face Analysis</h2>
+ <p>
+ {'Put yourself under the microscope of various facial recognition algorithms. See what can be determined from a photo.'}
+ </p>
+ <ol>
+ <li>Upload a photo of yourself and be judged by the algorithm</li>
+ <li>{'Your search data is only stored for the duration of this analysis and is 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.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..d9be7447
--- /dev/null
+++ b/client/faceAnalysis/faceAnalysis.reducer.js
@@ -0,0 +1,48 @@
+import * as types from '../types'
+
+const initialState = () => ({
+ query: {},
+ task: {},
+ result: {},
+ loading: false,
+ startTime: 0,
+ timing: 0,
+})
+
+export default function faceAnalysisReducer(state = initialState(), action) {
+ const { startTime } = state
+ switch (action.type) {
+ case types.faceAnalysis.loading:
+ return {
+ ...state,
+ startTime: action.ts,
+ timing: 0,
+ [action.tag]: { loading: true },
+ }
+
+ case types.faceAnalysis.loaded:
+ return {
+ ...state,
+ timing: action.ts - startTime,
+ [action.tag]: action.data,
+ }
+
+ case types.faceAnalysis.poll:
+ return {
+ ...state,
+ timing: action.ts - startTime,
+ result: action.data,
+ }
+
+ case types.faceAnalysis.error:
+ console.log('error', action)
+ return {
+ ...state,
+ timing: action.ts - startTime,
+ [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..9d77f258
--- /dev/null
+++ b/client/faceAnalysis/faceAnalysis.result.js
@@ -0,0 +1,131 @@
+import React, { Component } from 'react'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import * as actions from './faceAnalysis.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>
+ ),
+ not_an_image: (
+ <div>
+ <h2>{"Not an image"}</h2>
+ {"Sorry, the file you uploaded was not recognized as an image. Please upload a JPG or PNG image and try again."}
+ </div>
+ ),
+}
+
+class FaceAnalysisResult extends Component {
+ render() {
+ const { result, timing } = this.props
+ const { data, error, loading, message } = result
+ let { step, total } = data || {}
+ // console.log(step, total)
+ if (loading) {
+ return (
+ <div className='result'>
+ <div>
+ <Loader /><br />
+ <h2>Uploading...</h2>
+ </div>
+ </div>
+ )
+ }
+ if (error) {
+ // console.log(error)
+ let errorMessage = errors[error] || errors.error
+ return (
+ <div className='result'>{errorMessage}</div>
+ )
+ }
+ // console.log(result)
+ if (!total) {
+ return (
+ <div className='result'></div>
+ )
+ }
+
+ console.log(data.data)
+ const results = [
+ 'blur_fn', 'points_3d_68', 'landmarks_3d_68', 'landmarks_2d_68', 'pose',
+ ].map(tag => {
+ if (tag in data.data) {
+ const { title, url } = data.data[tag]
+ return (
+ <div key={tag}>
+ <img src={url} />
+ <span>{title}</span>
+ </div>
+ )
+ }
+ return null
+ }).filter(a => a)
+
+ const statisticsLabels = ['Age (Real)', 'Age (Apparent)', 'Gender', 'Beauty score', 'Emotion']
+ const statistics = [
+ 'age_real', 'age_apparent', 'gender', 'beauty', 'emotion'
+ ].map((tag, i) => {
+ if (tag in data.data.statistics) {
+ return (
+ <tr key={tag}>
+ <td>
+ {statisticsLabels[i]}
+ </td>
+ <td>
+ {data.data.statistics[tag]}
+ </td>
+ </tr>
+ )
+ }
+ return null
+ }).filter(a => a)
+
+ return (
+ <div>
+ <div className='results'>
+ {results}
+ </div>
+ {!!statistics.length && (
+ <table>
+ {statistics}
+ </table>
+ )}
+ <div className="about">
+ <small>Step {step} / {total} {message}</small><br />
+ <small>Query {step === total ? 'took' : 'timer:'} {(timing / 1000).toFixed(2)} s.</small>
+ </div>
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ query: state.faceAnalysis.query,
+ result: state.faceAnalysis.result,
+ timing: state.faceAnalysis.timing,
+ 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,
+}
diff --git a/client/faceSearch/faceSearch.query.js b/client/faceSearch/faceSearch.query.js
index 9f778ca0..2d140813 100644
--- a/client/faceSearch/faceSearch.query.js
+++ b/client/faceSearch/faceSearch.query.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
-import { Loader } from '../common'
+import { Loader, UploadImage } from '../common'
import * as actions from './faceSearch.actions'
// function parse_bbox(s) {
@@ -18,23 +18,8 @@ class FaceSearchQuery extends Component {
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)
+ upload(blob) {
+ this.props.actions.upload(this.props.payload, blob)
}
render() {
@@ -50,22 +35,15 @@ class FaceSearchQuery extends Component {
return (
<div className='query row'>
<div className='uploadContainer'>
- {result.loading ?
- <div className='loading' style={style}>
- <Loader />
- </div>
- : <div style={style}>
+ <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
- />
+ <UploadImage onUpload={this.upload.bind(this)} />
</div>
- }
+ {result.loading && (
+ <div className='loading' style={style}>
+ <Loader />
+ </div>
+ )}
</div>
<div className='cta'>
<h2>Search by Image</h2>
diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js
index 95534830..c2509033 100644
--- a/client/faceSearch/faceSearch.result.js
+++ b/client/faceSearch/faceSearch.result.js
@@ -72,8 +72,8 @@ class FaceSearchResult extends Component {
}
const els = results.map((result, i) => {
const distance = distances[i]
- const { uuid } = result.uuid
- const { x, y, w, h } = result.roi
+ const { uuid } = result.file_record
+ const { x, y, w, h } = result.face_roi
const { fullname, gender, description, images } = result.identity
const bbox = {
left: (100 * x) + '%',
diff --git a/client/index.js b/client/index.js
index 40be2841..96f2c8c8 100644
--- a/client/index.js
+++ b/client/index.js
@@ -20,6 +20,8 @@ function appendReactApplet(el, payload) {
}
function fetchDataset(payload) {
+ if (payload.command === 'face_analysis') return new Promise(resolve => resolve())
+ if (payload.dataset === 'info') return new Promise(resolve => resolve())
const url = "https://megapixels.nyc3.digitaloceanspaces.com/v1/citations/" + payload.dataset + ".json"
return fetch(url, { mode: 'cors' }).then(r => r.json())
}
diff --git a/client/nameSearch/nameSearch.query.js b/client/nameSearch/nameSearch.query.js
index 629b7b1d..99c1da84 100644
--- a/client/nameSearch/nameSearch.query.js
+++ b/client/nameSearch/nameSearch.query.js
@@ -11,8 +11,8 @@ class NameSearchQuery extends Component {
handleInput(value) {
this.setState({ q: value })
- if (value.strip().length > 1) {
- this.props.actions.search(this.props.payload, value.strip())
+ if (value.trim().length > 1) {
+ this.props.actions.search(this.props.payload, value.trim())
}
}
diff --git a/client/nameSearch/nameSearch.result.js b/client/nameSearch/nameSearch.result.js
index 9e20228c..38c544cc 100644
--- a/client/nameSearch/nameSearch.result.js
+++ b/client/nameSearch/nameSearch.result.js
@@ -50,7 +50,7 @@ class NameSearchResult extends Component {
)
}
const els = results.map((result, i) => {
- const { uuid } = result.uuid
+ const { uuid } = result.file_record
const { fullname, gender, description, images } = result.identity
return (
<div key={i}>
diff --git a/client/store.js b/client/store.js
index 13612f2d..e896bc58 100644
--- a/client/store.js
+++ b/client/store.js
@@ -1,10 +1,12 @@
import { applyMiddleware, compose, combineReducers, createStore } from 'redux'
import thunk from 'redux-thunk'
+import faceAnalysisReducer from './faceAnalysis/faceAnalysis.reducer'
import faceSearchReducer from './faceSearch/faceSearch.reducer'
import nameSearchReducer from './nameSearch/nameSearch.reducer'
const rootReducer = combineReducers({
+ faceAnalysis: faceAnalysisReducer,
faceSearch: faceSearchReducer,
nameSearch: nameSearchReducer,
})
diff --git a/client/types.js b/client/types.js
index fb1fbe30..fd9aa3e0 100644
--- a/client/types.js
+++ b/client/types.js
@@ -6,6 +6,10 @@ export const tagAsType = (type, names) => (
}, {})
)
+export const faceAnalysis = tagAsType('faceAnalysis', [
+ 'loading', 'loaded', 'poll', 'error', 'update_options',
+])
+
export const faceSearch = tagAsType('faceSearch', [
'loading', 'loaded', 'error', 'update_options',
])
diff --git a/client/util.js b/client/util.js
index f181ad0f..d0db0d98 100644
--- a/client/util.js
+++ b/client/util.js
@@ -82,6 +82,21 @@ export const preloadImage = opt => {
/* AJAX */
+export const get = (uri, data) => {
+ let headers = {
+ Accept: 'application/json, application/xml, text/play, text/html, *.*',
+ }
+ let opt = {
+ method: 'GET',
+ body: data,
+ headers,
+ // credentials: 'include',
+ }
+ // console.log(headers)
+ // headers['X-CSRFToken'] = csrftoken
+ return fetch(uri, opt).then(res => res.json())
+}
+
export const post = (uri, data) => {
let headers
if (data instanceof FormData) {
@@ -99,7 +114,7 @@ export const post = (uri, data) => {
method: 'POST',
body: data,
headers,
- credentials: 'include',
+ // credentials: 'include',
}
// console.log(headers)
// headers['X-CSRFToken'] = csrftoken