From 76f36c6c5dafe754b066903b1ee8ecdd1b92dcab Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 16 Dec 2018 20:01:23 +0100 Subject: faceSearch client --- client/faceSearch/faceSearch.actions.js | 57 ++++++++++++++++++++++++ client/faceSearch/faceSearch.container.js | 31 +++++++++++++ client/faceSearch/faceSearch.query.js | 73 +++++++++++++++++++++++++++++++ client/faceSearch/faceSearch.reducer.js | 32 ++++++++++++++ client/faceSearch/faceSearch.result.js | 30 +++++++++++++ client/faceSearch/index.js | 5 +++ 6 files changed, 228 insertions(+) create mode 100644 client/faceSearch/faceSearch.actions.js create mode 100644 client/faceSearch/faceSearch.container.js create mode 100644 client/faceSearch/faceSearch.query.js create mode 100644 client/faceSearch/faceSearch.reducer.js create mode 100644 client/faceSearch/faceSearch.result.js create mode 100644 client/faceSearch/index.js (limited to 'client/faceSearch') diff --git a/client/faceSearch/faceSearch.actions.js b/client/faceSearch/faceSearch.actions.js new file mode 100644 index 00000000..ccd51201 --- /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 = (file, query) => 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(), 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..e5fae24b --- /dev/null +++ b/client/faceSearch/faceSearch.container.js @@ -0,0 +1,31 @@ +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() { + return ( +
+ + +
+ ) + } +} + +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)(FaceSearchContainer) diff --git a/client/faceSearch/faceSearch.query.js b/client/faceSearch/faceSearch.query.js new file mode 100644 index 00000000..9313c538 --- /dev/null +++ b/client/faceSearch/faceSearch.query.js @@ -0,0 +1,73 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from './faceSearch.actions' + +class FaceSearchQuery 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 + this.props.actions.upload(file) + } + + render() { + const { result } = this.props + return ( +
+
+ {result.loading ? +
+ Loading... +
+ : +
+ + +
+ } +
+
+

Search This Dataset

+

Searching {13456} images

+

+ Use facial recognition to reverse search into the LFW dataset and see if it contains your photos. +

+
    +
  1. Upload a photo of yourself
  2. +
  3. Use a photo similar to examples below
  4. +
  5. Only matches over 85% will be displayed
  6. +
  7. Read more tips to improve search results
  8. +
  9. Your search data is never stored and immediately cleared once you leave this page.
  10. +
+

+ Read more about privacy. +

+
+
+ ) + } +} + +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..844a5a70 --- /dev/null +++ b/client/faceSearch/faceSearch.result.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from './faceSearch.actions' + +class FaceSearchResult extends Component { + componentDidMount() { + } + + render() { + return ( +
+ Result here +
+ ) + } +} + +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, +} -- cgit v1.2.3-70-g09d2 From f9e4b621c9daf66a599c28481b3bf59926144461 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 16 Dec 2018 20:30:25 +0100 Subject: query html --- client/faceSearch/faceSearch.actions.js | 10 ++++---- client/faceSearch/faceSearch.container.js | 17 ++++---------- client/faceSearch/faceSearch.query.js | 3 ++- client/util.js | 39 +------------------------------ 4 files changed, 13 insertions(+), 56 deletions(-) (limited to 'client/faceSearch') diff --git a/client/faceSearch/faceSearch.actions.js b/client/faceSearch/faceSearch.actions.js index ccd51201..224977b5 100644 --- a/client/faceSearch/faceSearch.actions.js +++ b/client/faceSearch/faceSearch.actions.js @@ -40,16 +40,16 @@ export const updateOptions = opt => dispatch => { // API functions -export const upload = (file, query) => dispatch => { +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(), fd) + // if (!query) { + dispatch(loading(tag)) + // } + post(url.upload(payload.dataset), fd) .then(data => { dispatch(loaded(tag, data)) }) diff --git a/client/faceSearch/faceSearch.container.js b/client/faceSearch/faceSearch.container.js index e5fae24b..f96961db 100644 --- a/client/faceSearch/faceSearch.container.js +++ b/client/faceSearch/faceSearch.container.js @@ -9,23 +9,16 @@ import FaceSearchResult from './faceSearch.result' class FaceSearchContainer extends Component { render() { + const { payload } = this.props + console.log(payload) return (
- - + +
) } } -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)(FaceSearchContainer) +export default FaceSearchContainer diff --git a/client/faceSearch/faceSearch.query.js b/client/faceSearch/faceSearch.query.js index 9313c538..1261269d 100644 --- a/client/faceSearch/faceSearch.query.js +++ b/client/faceSearch/faceSearch.query.js @@ -6,6 +6,7 @@ import * as actions from './faceSearch.actions' class FaceSearchQuery extends Component { upload(e) { + const { payload } = this.props const files = e.dataTransfer ? e.dataTransfer.files : e.target.files let i let file @@ -14,7 +15,7 @@ class FaceSearchQuery extends Component { if (file && file.type.match('image.*')) break } if (!file) return - this.props.actions.upload(file) + this.props.actions.upload(this.props.payload, file) } render() { diff --git a/client/util.js b/client/util.js index 4ce39ada..f181ad0f 100644 --- a/client/util.js +++ b/client/util.js @@ -40,10 +40,6 @@ export const pad = (n, m) => { return s } -// Verified is 0/1 when retrieved from SQL, but 'verified' or 'unverified' when retrieved elsewhere -export const isVerified = verified => verified === 1 || verified === '1' || verified === 'verified' -export const verify = verified => isVerified(verified) ? 'verified' : 'unverified' - export const courtesyS = (n, s) => n + ' ' + (n === 1 ? s : s + 's') export const padSeconds = n => n < 10 ? '0' + n : n @@ -59,37 +55,11 @@ export const timestamp = (n = 0, fps = 25) => { } export const percent = n => (n * 100).toFixed(1) + '%' - export const px = (n, w) => Math.round(n * w) + 'px' - export const clamp = (n, a, b) => n < a ? a : n < b ? n : b /* URLs */ -export const hashPath = sha256 => { - if (!sha256 || sha256.length < 9) { - throw new Error('Invalid sha256') - } - return [ - sha256.slice(0, 3), - sha256.slice(3, 6), - sha256.slice(6, 9), - sha256, - ].join('/') -} - -export const imageUrl = (verified, sha256, frame, size = 'th') => [ - 'https://' + process.env.S3_HOST + '/v1/media/keyframes', - isVerified(verified) ? null : 'unverified', - hashPath(sha256), - pad(frame, 6), - size, - 'index.jpg' -].filter(s => !!s).join('/') - -export const metadataUri = (sha256, tag) => '/metadata/' + sha256 + '/' + tag + '/' -export const keyframeUri = (sha256, frame) => '/metadata/' + sha256 + '/keyframe/' + pad(frame, 6) + '/' - export const preloadImage = opt => { let { verified, hash, frame, url } = opt if (hash && frame) { @@ -112,11 +82,7 @@ export const preloadImage = opt => { /* AJAX */ -let cachedAuth = null -let token = '' -let username = '' - -export const post = (uri, data, credentials) => { +export const post = (uri, data) => { let headers if (data instanceof FormData) { headers = { @@ -135,9 +101,6 @@ export const post = (uri, data, credentials) => { headers, credentials: 'include', } - if (credentials) { - headers.Authorization = 'Token ' + token - } // console.log(headers) // headers['X-CSRFToken'] = csrftoken return fetch(uri, opt).then(res => res.json()) -- cgit v1.2.3-70-g09d2 From 0bbaef7c889f2bf17cdf7e4584a6946085d0a7eb Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Sun, 16 Dec 2018 23:04:38 +0100 Subject: display img --- client/common/activeLink.component.js | 16 -- client/common/common.css | 348 +--------------------------------- client/common/footer.component.js | 10 - client/common/index.js | 11 -- client/common/keyframe.component.js | 118 ------------ client/common/keyframes.component.js | 95 ---------- client/common/loader.component.js | 5 +- client/common/sidebar.component.js | 21 +- client/faceSearch/faceSearch.query.js | 23 ++- 9 files changed, 25 insertions(+), 622 deletions(-) delete mode 100644 client/common/activeLink.component.js delete mode 100644 client/common/footer.component.js delete mode 100644 client/common/keyframe.component.js delete mode 100644 client/common/keyframes.component.js (limited to 'client/faceSearch') diff --git a/client/common/activeLink.component.js b/client/common/activeLink.component.js deleted file mode 100644 index 59f63881..00000000 --- a/client/common/activeLink.component.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import { NavLink } from 'react-router-dom' - -export default function ActiveLink({ - to, - className = 'navlink', - children -}) { - return ( - - - {children} - - - ) -} diff --git a/client/common/common.css b/client/common/common.css index 4b939df0..56cf2fe9 100644 --- a/client/common/common.css +++ b/client/common/common.css @@ -1,347 +1 @@ -/* 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; -} - -h1 { - -} -h2 { - font-weight: normal; - margin: 10px 0; - padding: 3px; - font-size: 24px; -} -h3 { - font-weight: normal; - margin: 10px 0 0 0; - padding: 3px; - font-size: 18px; -} -h4 { - font-weight: 300; - font-size: 12px; - letter-spacing: 2px; - color: #888; - text-transform: uppercase; - margin: 5px 10px; - margin-top: 20px; -} -h4:first-child { - margin-top: 10px; -} - -.app { - width: 100%; - height: 100%; - display: flex; - flex-direction: row; - align-items: flex-start; - justify-content: flex-start; -} - -/* header stuff */ - -header { - 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; -} - -/* sidebar / body columns */ - -.sidebar { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: flex-start; - height: 100%; - float: left; - width: 200px; - flex: 0 0 200px; - padding: 10px; - margin-right: 10px; -} -.sidebar a { - display: block; - padding: 10px 10px; - text-decoration: none; - color: #444; -} -.sidebar a.active { - font-weight: bold; - color: #222; -} -.body { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - flex-grow: 1; -} -.body > div { - padding-bottom: 40px; -} - -/* buttons / forms */ - -.btn:focus, .btn:hover { - background: #f1f1fc; - color: #4b48d6 !important; - text-decoration: none; -} -.btn { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background: #fff; - border: .05rem solid; - border-radius: 2px; - margin-right: 5px; - color: #11f; - cursor: pointer; - display: inline-block; - font-size: .8rem; - height: 1.8rem; - line-height: 1rem; - outline: none; - padding: .35rem .4rem; - text-align: center; - text-decoration: none; - -webkit-transition: all .2s ease; - -o-transition: all .2s ease; - transition: all .2s ease; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - vertical-align: middle; - white-space: nowrap; -} -.btn.reset, -.btn.panic { - color: #b00; -} -.btn.btn-primary { - background: #11f; - border-color: #11f; - color: white; -} -.btn[disabled] { - color: #bbb !important; - border-color: #bbb !important; - background: white !important; - cursor: default; -} -.btn.btn-primary:focus, -.btn.btn-primary:hover { - background: #0808ee; - color: white !important; -} -.row .btn { - margin: 0 5px 0 0; -} -input[type=text] { - border: 1px solid #888; - padding: 4px; - font-size: 15px; -} - - -/* tables on metadata pages */ - -table { - border: 0; - margin: 0; - padding: 0; - border-spacing: 0; -} -.tableObject td, -.tableObject th { - padding: 3px; - vertical-align: top; -} -.tableObject hr { - width: 100%; - color: transparent; - border: 0; - border-bottom: 1px solid #bbb; - align: left; - margin: 3px 0; - padding: 0; -} -.tableObject th, -.tableTuples th { - min-width: 145px; - text-align: left; - text-transform: capitalize; - padding: 3px; - padding-right: 10px; - font-weight: 300; - color: #333; -} -.tableTuples td { - text-align: right; - padding: 3px; -} -.tableObject td { - font-weight: normal; - color: #000; -} -.tableObject .tableObject { - border: 1px solid #ddd; -} -.tableArray { - border: 1px solid #ddd; - border-spacing: 0; -} -.tableArray td { - border-bottom: 1px solid #ddd; -} -.gray { - font-size: 12px; - color: #888; - display: block; -} -.sha256.heading { - margin: 20px 0 0px; -} -.gray span { - padding-right: 5px; -} -.gray { - margin-bottom: 10px; -} -.gray a { - color: #666; -} - -.verified { - color: #080; - font-weight: bold; -} -.unverified { - color: #f00; - font-weight: 300; -} - -.loading, .error { - font-weight: normal; - margin: 10px 0; - padding: 3px; - font-size: 24px; -} - -.title { - text-transform: capitalize; -} -.rect { - position: absolute; -} -.rect { border: 1px solid rgba(0,0,255); background-color: rgba(0,0,255,0.1); } - -/* videos / video preloader */ - -video { - max-width: 640px; - margin: 10px 0; -} -.video { - margin: 0 0 10px 0; -} -.video .bg { - cursor: pointer; - position: relative; - background-size: cover; -} -.video .play { - position: absolute; - top: 50%; - left: 50%; - transform: translate3d(-50%, -50%, 0); - width: 20%; - height: 20%; - background-image: url(/search/static/img/play.png); - background-position: center center; - background-size: contain; - background-repeat: no-repeat; -} -.desktop .video .play:hover { - -webkit-filter: invert(60%) sepia(100%) saturate(500%) hue-rotate(160deg); -} - -/* spectre.css loader */ - -.loaderWrapper { - display: inline-block; - position: relative; - width: .8rem; - height: .8rem; - padding: 10px; -} -.loader { - color: transparent !important; - min-height: .8rem; - pointer-events: none; - position: relative; -} - -.loader::after { - animation: loader 500ms infinite linear; - border: .1rem solid #5755d9; - border-radius: 50%; - border-right-color: transparent; - border-top-color: transparent; - content: ""; - display: block; - height: .8rem; - left: 50%; - margin-left: -.4rem; - margin-top: -.4rem; - position: absolute; - top: 50%; - width: .8rem; - z-index: 1; -} - -.loader.loader-lg { - min-height: 2rem; -} - -.loader.loader-lg::after { - height: 1.6rem; - margin-left: -.8rem; - margin-top: -.8rem; - width: 1.6rem; -} - -@keyframes loader { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} \ No newline at end of file +* {} \ No newline at end of file diff --git a/client/common/footer.component.js b/client/common/footer.component.js deleted file mode 100644 index 7c82b44b..00000000 --- a/client/common/footer.component.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { Link } from 'react-router-dom' -import { connect } from 'react-redux' - -export default function Footer(props) { - return ( -
-
- ); -} diff --git a/client/common/index.js b/client/common/index.js index ad9fe5e1..cfb34b32 100644 --- a/client/common/index.js +++ b/client/common/index.js @@ -1,23 +1,15 @@ -import Header from 'vcat-header' - -import ActiveLink from './activeLink.component' import Classifier from './classifier.component' import DetectionBoxes from './detectionBoxes.component' import DetectionList from './detectionList.component' // import Header from './header.component' -import Footer from './footer.component' import Loader from './loader.component' import Sidebar from './sidebar.component' import Gate from './gate.component' -import Keyframe from './keyframe.component' -import Keyframes from './keyframes.component' import Video from './video.component' import { TableObject, TableArray, TableTuples, TableRow, TableCell } from './table.component' import './common.css' export { - Header, - Footer, Sidebar, Loader, Gate, @@ -26,11 +18,8 @@ export { TableTuples, TableRow, TableCell, - ActiveLink, Classifier, DetectionList, DetectionBoxes, - Keyframe, - Keyframes, Video, } diff --git a/client/common/keyframe.component.js b/client/common/keyframe.component.js deleted file mode 100644 index c77db3ac..00000000 --- a/client/common/keyframe.component.js +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react' -import { Link } from 'react-router-dom' -import { imageUrl, timestamp, keyframeUri, widths, verify } from '../util' -import { DetectionBoxes } from '.' - -import * as searchActions from '../search/search.actions' - -export default function Keyframe({ - verified, - sha256, - frame, - score, - isSaved, - fps = 25, - size = 'th', - className, - showHash, - showFrame, - showTimestamp, - showScore, - showSearchButton, - showSaveButton, - to, - children, - detectionList = [], - aspectRatio = 1.777, - onClick, - reviewActions, -}) { - if (!sha256) return null - const width = widths[size] - const height = Math.round(width / aspectRatio) - return ( -
-
- - {'Frame - {detectionList.map(({ labels, detections }, i) => ( - - ))} - - {(reviewActions && (showSearchButton || showSaveButton)) && - - } -
- {(showHash || showFrame || showTimestamp || showScore) && - - } - {children} -
- ) -} - -const PossiblyExternalLink = props => { - if (props.onClick) { - return props.children - } - if (props.to.match(/^http/)) { - return {props.children} - } - return -} diff --git a/client/common/keyframes.component.js b/client/common/keyframes.component.js deleted file mode 100644 index 62eda45e..00000000 --- a/client/common/keyframes.component.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react' -import { Link } from 'react-router-dom' -import { bindActionCreators } from 'redux' -import { connect } from 'react-redux' - -import { Keyframe } from '.' -import * as reviewActions from '../review/review.actions' -import * as searchActions from '../search/search.actions' - -function Keyframes(props) { - // console.log(props) - let { - frames, - groupByHash, - } = props - let minDistance = 0 - if (frames && frames.length) { - minDistance = frames[0].distance || 0 - } - if (!groupByHash) { - return ( - - ) - } - const frameGroups = frames.reduce((a, b) => { - if (a[b.hash]) { - a[b.hash].push(b) - } else { - a[b.hash] = [b] - } - return a - }, {}) - return Object.keys(frameGroups) - .map(hash => [frameGroups[hash].length, hash]) - .sort((a, b) => b[0] - a[0]) - .map(([count, hash]) => ( - - )) -} - -function KeyframeList(props) { - let { - saved = {}, - frames, - options, - review, - search, - minDistance, - label, - count, - ...frameProps - } = props - if (!frames) return null - return ( -
- {label &&

{label} ({count})

} - {frames.map(({ hash, frame, verified, distance }) => ( - review.toggleSaved({ verified, hash, frame })} - reviewActions={review} - {...frameProps} - /> - ))} -
- ) -} - -const mapStateToProps = state => ({ - saved: state.review.saved, - options: state.search.options, -}) - -const mapDispatchToProps = dispatch => ({ - review: bindActionCreators({ ...reviewActions }, dispatch), - search: bindActionCreators({ ...searchActions }, dispatch), -}) - -export default connect(mapStateToProps, mapDispatchToProps)(Keyframes) diff --git a/client/common/loader.component.js b/client/common/loader.component.js index 6795424b..5930c63e 100644 --- a/client/common/loader.component.js +++ b/client/common/loader.component.js @@ -1,10 +1,11 @@ -import React, { Component } from 'react' +import React from 'react' export default function Loader() { return (
+
) -} \ No newline at end of file +} diff --git a/client/common/sidebar.component.js b/client/common/sidebar.component.js index 487f3289..afbf8c8c 100644 --- a/client/common/sidebar.component.js +++ b/client/common/sidebar.component.js @@ -1,32 +1,13 @@ import React, { Component } from 'react' -import { NavLink } from 'react-router-dom' import { connect } from 'react-redux' class Sidebar extends Component { render() { - const { hash } = this.props - if (!hash) { - return ( -
-
- ) - } return (
-

Media

- Summary - Media Record - Media Info - Sugarcube - -

Keyframes

- Keyframe - -

Detectors

- Places 365 - Coco
) + // Summary } } diff --git a/client/faceSearch/faceSearch.query.js b/client/faceSearch/faceSearch.query.js index 1261269d..8302e437 100644 --- a/client/faceSearch/faceSearch.query.js +++ b/client/faceSearch/faceSearch.query.js @@ -2,9 +2,14 @@ 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 @@ -15,20 +20,32 @@ class FaceSearchQuery extends Component { if (file && file.type.match('image.*')) break } if (!file) return + var 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' + } return (
{result.loading ? -
- Loading... +
+
: -
+
Date: Mon, 17 Dec 2018 00:35:19 +0100 Subject: returning results...! --- client/faceSearch/faceSearch.query.js | 26 +++++++------ megapixels/app/models/sql_factory.py | 64 ++++++++++++++++++++++++++++--- megapixels/app/processors/faiss.py | 58 ++++++++++++++++++++++++++++ megapixels/app/server/api.py | 53 ++++++++++++++++++++----- megapixels/app/server/json_encoder.py | 17 ++++++++ megapixels/commands/faiss/build_faiss.py | 36 +---------------- site/assets/img/ajax-loader.gif | Bin 1849 -> 0 bytes site/assets/img/loader.gif | Bin 0 -> 1849 bytes 8 files changed, 193 insertions(+), 61 deletions(-) create mode 100644 megapixels/app/processors/faiss.py create mode 100644 megapixels/app/server/json_encoder.py delete mode 100644 site/assets/img/ajax-loader.gif create mode 100644 site/assets/img/loader.gif (limited to 'client/faceSearch') diff --git a/client/faceSearch/faceSearch.query.js b/client/faceSearch/faceSearch.query.js index 8302e437..425cb282 100644 --- a/client/faceSearch/faceSearch.query.js +++ b/client/faceSearch/faceSearch.query.js @@ -20,12 +20,12 @@ class FaceSearchQuery extends Component { if (file && file.type.match('image.*')) break } if (!file) return - var fr = new FileReader(); + const fr = new FileReader() fr.onload = () => { fr.onload = null this.setState({ image: fr.result }) } - fr.readAsDataURL(files[0]); + fr.readAsDataURL(files[0]) this.props.actions.upload(this.props.payload, file) } @@ -36,6 +36,7 @@ class FaceSearchQuery extends Component { if (image) { style.backgroundImage = 'url(' + image + ')' style.backgroundSize = 'cover' + style.opacity = 1 } return (
@@ -44,9 +45,8 @@ class FaceSearchQuery extends Component {
- : -
- + :
+ {image ? null : } }
-
+

Search This Dataset

Searching {13456} images

- Use facial recognition to reverse search into the LFW dataset and see if it contains your photos. + {'Use facial recognition to reverse search into the LFW dataset '} + {'and see if it contains your photos.'}

    -
  1. Upload a photo of yourself
  2. -
  3. Use a photo similar to examples below
  4. -
  5. Only matches over 85% will be displayed
  6. -
  7. Read more tips to improve search results
  8. -
  9. Your search data is never stored and immediately cleared once you leave this page.
  10. +
  11. Upload a photo of yourself
  12. +
  13. Use a photo similar to examples below
  14. +
  15. Only matches over 85% will be displayed
  16. +
  17. Read more tips to improve search results
  18. +
  19. {'Your search data is never stored and immediately cleared '} + {'once you leave this page.'}

Read more about privacy. diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index e35c3e15..0f7e73a0 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -19,6 +19,7 @@ connection_url = "mysql+mysqldb://{}:{}@{}/{}".format( datasets = {} loaded = False +Session = None def list_datasets(): return [dataset.describe() for dataset in datasets.values()] @@ -31,10 +32,11 @@ def get_table(name, table_name): return dataset.get_table(table_name) if dataset else None def load_sql_datasets(replace=False, base_model=None): - global datasets, loaded + global datasets, loaded, Session if loaded: return datasets - engine = create_engine(connection_url) if replace else None + engine = create_engine(connection_url) + Session = sessionmaker(bind=engine) for path in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): dataset = load_sql_dataset(path, replace, engine, base_model) datasets[dataset.name] = dataset @@ -79,6 +81,27 @@ class SqlDataset: 'tables': list(self.tables.keys()), } + def get_identity(self, id): + table = self.get_table('identity_meta') + identity = table.query.filter(table.image_id >= id).order_by(table.image_id.asc()).first().toJSON() + print(identity) + return { + 'uuid': self.select('uuids', id), + 'identity': identity, + 'roi': self.select('roi', id), + 'pose': self.select('pose', id), + } + + def select(self, table, id): + table = self.get_table(table) + if not table: + return None + session = Session() + # for obj in session.query(table).filter_by(id=id): + print(table) + obj = session.query(table).filter(table.id == id).first() + return obj.toJSON() + def get_table(self, type): if type in self.tables: return self.tables[type] @@ -102,6 +125,11 @@ class SqlDataset: __tablename__ = self.name + "_uuid" id = Column(Integer, primary_key=True) uuid = Column(String(36), nullable=False) + def toJSON(self): + return { + 'id': self.id, + 'uuid': self.uuid, + } return UUID # ==> roi.csv <== @@ -118,6 +146,17 @@ class SqlDataset: w = Column(Float, nullable=False) x = Column(Float, nullable=False) y = Column(Float, nullable=False) + def toJSON(self): + return { + 'id': self.id, + 'image_index': self.image_index, + 'image_height': self.image_height, + 'image_width': self.image_width, + 'w': self.w, + 'h': self.h, + 'x': self.x, + 'y': self.y, + } return ROI # ==> identity.csv <== @@ -132,6 +171,15 @@ class SqlDataset: gender = Column(String(1), nullable=False) images = Column(Integer, nullable=False) image_id = Column(Integer, nullable=False) + def toJSON(self): + return { + 'id': self.id, + 'image_id': self.image_id, + 'fullname': self.fullname, + 'images': self.images, + 'gender': self.gender, + 'description': self.description, + } return Identity # ==> pose.csv <== @@ -145,8 +193,12 @@ class SqlDataset: pitch = Column(Float, nullable=False) roll = Column(Float, nullable=False) yaw = Column(Float, nullable=False) + def toJSON(self): + return { + 'id': self.id, + 'image_id': self.image_id, + 'pitch': self.pitch, + 'roll': self.roll, + 'yaw': self.yaw, + } return Pose - - -# Session = sessionmaker(bind=engine) -# session = Session() diff --git a/megapixels/app/processors/faiss.py b/megapixels/app/processors/faiss.py new file mode 100644 index 00000000..5156ad71 --- /dev/null +++ b/megapixels/app/processors/faiss.py @@ -0,0 +1,58 @@ +""" +Index all of the FAISS datasets +""" + +import os +import glob +import faiss +import time +import numpy as np + +from app.utils.file_utils import load_recipe, load_csv_safe +from app.settings import app_cfg as cfg + +class DefaultRecipe: + def __init__(self): + self.dim = 128 + self.factory_type = 'Flat' + +def build_all_faiss_databases(): + datasets = [] + for fn in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): + name = os.path.basename(fn) + recipe_fn = os.path.join(cfg.DIR_FAISS_RECIPES, name + ".json") + if os.path.exists(recipe_fn): + build_faiss_database(name, load_recipe(recipe_fn)) + else: + build_faiss_database(name, DefaultRecipe()) + +def build_faiss_database(name, recipe): + vec_fn = os.path.join(cfg.DIR_FAISS_METADATA, name, "vecs.csv") + index_fn = os.path.join(cfg.DIR_FAISS_INDEXES, name + ".index") + + index = faiss.index_factory(recipe.dim, recipe.factory_type) + + keys, rows = load_csv_safe(vec_fn) + feats = np.array([ list(map(float, row[3].split(","))) for row in rows ]).astype('float32') + n, d = feats.shape + + print("{}: training {} x {} dim vectors".format(name, n, d)) + print(recipe.factory_type) + + add_start = time.time() + index.add(feats) + add_end = time.time() + add_time = add_end - add_start + print("{}: add time: {:.1f}s".format(name, add_time)) + + faiss.write_index(index, index_fn) + +def load_faiss_databases(): + faiss_datasets = {} + for fn in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): + name = os.path.basename(fn) + index_fn = os.path.join(cfg.DIR_FAISS_INDEXES, name + ".index") + if os.path.exists(index_fn): + index = faiss.read_index(index_fn) + faiss_datasets[name] = index + return faiss_datasets diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index cf8241bd..36563910 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -2,18 +2,23 @@ import os import re import time import dlib +import numpy as np from flask import Blueprint, request, jsonify from PIL import Image # todo: try to remove PIL dependency from app.processors import face_recognition from app.processors import face_detector -from app.models.sql_factory import list_datasets, get_dataset, get_table +from app.processors.faiss import load_faiss_databases +from app.models.sql_factory import load_sql_datasets, list_datasets, get_dataset, get_table +from app.utils.im_utils import pil2np sanitize_re = re.compile('[\W]+') valid_exts = ['.gif', '.jpg', '.jpeg', '.png'] api = Blueprint('api', __name__) +faiss_datasets = load_faiss_databases() + @api.route('/') def index(): return jsonify({ 'datasets': list_datasets() }) @@ -26,10 +31,15 @@ def show(name): else: return jsonify({ 'status': 404 }) -@api.route('/dataset//face', methods=['POST']) +@api.route('/dataset//face/', methods=['POST']) def upload(name): start = time.time() dataset = get_dataset(name) + if name not in faiss_datasets: + return jsonify({ + 'error': 'invalid dataset' + }) + faiss_dataset = faiss_datasets[name] file = request.files['query_img'] fn = file.filename if fn.endswith('blob'): @@ -40,22 +50,46 @@ def upload(name): if ext.lower() not in valid_exts: return jsonify({ 'error': 'not an image' }) - img = Image.open(file.stream).convert('RGB') + im = Image.open(file.stream).convert('RGB') + im_np = pil2np(im) # Face detection detector = face_detector.DetectorDLIBHOG() # get detection as BBox object - bboxes = detector.detect(im, largest=True) + bboxes = detector.detect(im_np, largest=True) bbox = bboxes[0] - dim = im.shape[:2][::-1] + dim = im_np.shape[:2][::-1] bbox = bbox.to_dim(dim) # convert back to real dimensions # face recognition/vector recognition = face_recognition.RecognitionDLIB(gpu=-1) + vec = recognition.vec(im_np, bbox) + + # print(vec) + query = np.array([ vec ]).astype('float32') + + # query FAISS! + distances, indexes = faiss_dataset.search(query, 5) + + if len(indexes) == 0: + print("weird, no results!") + return [] + + # get the results for this single query... + distances = distances[0] + indexes = indexes[0] - # print(vec.shape) - # results = db.search(vec, limit=limit) + if len(indexes) == 0: + print("no results!") + return [] + + lookup = {} + for _d, _i in zip(distances, indexes): + lookup[_i+1] = _d + + print(distances) + print(indexes) # with the result we have an ID # query the sql dataset for the UUID etc here @@ -63,12 +97,13 @@ def upload(name): query = { 'timing': time.time() - start, } - results = [] + results = [ dataset.get_identity(index) for index in indexes ] print(results) return jsonify({ - 'query': query, 'results': results, + # 'distances': distances.tolist(), + # 'indexes': indexes.tolist(), }) @api.route('/dataset//name', methods=['GET']) diff --git a/megapixels/app/server/json_encoder.py b/megapixels/app/server/json_encoder.py new file mode 100644 index 00000000..89af578a --- /dev/null +++ b/megapixels/app/server/json_encoder.py @@ -0,0 +1,17 @@ +from sqlalchemy.ext.declarative import DeclarativeMeta +from flask import json + +class AlchemyEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o.__class__, DeclarativeMeta): + data = {} + fields = o.__json__() if hasattr(o, '__json__') else dir(o) + for field in [f for f in fields if not f.startswith('_') and f not in ['metadata', 'query', 'query_class']]: + value = o.__getattribute__(field) + try: + json.dumps(value) + data[field] = value + except TypeError: + data[field] = None + return data + return json.JSONEncoder.default(self, o) diff --git a/megapixels/commands/faiss/build_faiss.py b/megapixels/commands/faiss/build_faiss.py index ec94c924..fc6b37ce 100644 --- a/megapixels/commands/faiss/build_faiss.py +++ b/megapixels/commands/faiss/build_faiss.py @@ -11,11 +11,7 @@ import numpy as np from app.utils.file_utils import load_recipe, load_csv_safe from app.settings import app_cfg as cfg - -class DefaultRecipe: - def __init__(self): - self.dim = 128 - self.factory_type = 'Flat' +from app.processors.faiss import build_all_faiss_databases @click.command() @click.pass_context @@ -25,32 +21,4 @@ def cli(ctx): - uses the recipe above by default - however you can override this by adding a new recipe in faiss/recipes/{name}.json """ - datasets = [] - for fn in glob.iglob(os.path.join(cfg.DIR_FAISS_METADATA, "*")): - name = os.path.basename(fn) - recipe_fn = os.path.join(cfg.DIR_FAISS_RECIPES, name + ".json") - if os.path.exists(recipe_fn): - build_faiss(name, load_recipe(recipe_fn)) - else: - build_faiss(name, DefaultRecipe()) - -def build_faiss(name, recipe): - vec_fn = os.path.join(cfg.DIR_FAISS_METADATA, name, "vecs.csv") - index_fn = os.path.join(cfg.DIR_FAISS_INDEXES, name + ".index") - - index = faiss.index_factory(recipe.dim, recipe.factory_type) - - keys, rows = load_csv_safe(vec_fn) - feats = np.array([ list(map(float, row[3].split(","))) for row in rows ]).astype('float32') - n, d = feats.shape - - print("{}: training {} x {} dim vectors".format(name, n, d)) - print(recipe.factory_type) - - add_start = time.time() - index.add(feats) - add_end = time.time() - add_time = add_end - add_start - print("{}: add time: {:.1f}s".format(name, add_time)) - - faiss.write_index(index, index_fn) + build_all_faiss_databases() diff --git a/site/assets/img/ajax-loader.gif b/site/assets/img/ajax-loader.gif deleted file mode 100644 index dc21df18..00000000 Binary files a/site/assets/img/ajax-loader.gif and /dev/null differ diff --git a/site/assets/img/loader.gif b/site/assets/img/loader.gif new file mode 100644 index 00000000..dc21df18 Binary files /dev/null and b/site/assets/img/loader.gif differ -- cgit v1.2.3-70-g09d2 From d7df4ee5b9e24a9cdf2bf4d1bc2e73e97352afdc Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 01:02:40 +0100 Subject: searches execute --- client/actions.js | 6 +----- client/faceSearch/faceSearch.result.js | 35 ++++++++++++++++++++++++++++++---- megapixels/app/models/sql_factory.py | 5 +++-- megapixels/app/server/api.py | 9 +++++---- site/assets/css/applets.css | 17 +++++++++++++++++ 5 files changed, 57 insertions(+), 15 deletions(-) (limited to 'client/faceSearch') diff --git a/client/actions.js b/client/actions.js index 37b4eb2e..bb011838 100644 --- a/client/actions.js +++ b/client/actions.js @@ -1,9 +1,5 @@ import * as faceSearch from './faceSearch/faceSearch.actions' -// import * as review from './review/review.actions' -// import * as metadata from './metadata/metadata.actions' export { - // search, - // review, - // metadata, + faceSearch } diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index 844a5a70..2b223a46 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -1,17 +1,44 @@ import React, { Component } from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' +import { courtesyS } from '../util' import * as actions from './faceSearch.actions' class FaceSearchResult extends Component { - componentDidMount() { - } - render() { + const { dataset } = this.props.payload + const { distances, results } = this.props.result + if (!results) { + return ( +

+ ) + } + if (!this.props.result.results.length) { + return ( +
No results
+ ) + } + const els = results.map((result, i) => { + const distance = distances[i] + const { uuid } = result.uuid + const { fullname, gender, description, images } = result.identity + return ( +
+ + {fullname} {'('}{gender}{')'}
+ {description}
+ {courtesyS(images, 'image')}
+ {distance} +
+ ) + }) + return (
- Result here +
+ {els} +
) } diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 0f7e73a0..9a44941b 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -83,8 +83,8 @@ class SqlDataset: def get_identity(self, id): table = self.get_table('identity_meta') - identity = table.query.filter(table.image_id >= id).order_by(table.image_id.asc()).first().toJSON() - print(identity) + # id += 1 + identity = table.query.filter(table.image_id <= id).order_by(table.image_id.desc()).first().toJSON() return { 'uuid': self.select('uuids', id), 'identity': identity, @@ -100,6 +100,7 @@ class SqlDataset: # for obj in session.query(table).filter_by(id=id): print(table) obj = session.query(table).filter(table.id == id).first() + session.close() return obj.toJSON() def get_table(self, type): diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 36563910..2f78ecd3 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -70,7 +70,7 @@ def upload(name): query = np.array([ vec ]).astype('float32') # query FAISS! - distances, indexes = faiss_dataset.search(query, 5) + distances, indexes = faiss_dataset.search(query, 10) if len(indexes) == 0: print("weird, no results!") @@ -85,6 +85,7 @@ def upload(name): return [] lookup = {} + ids = [i+1 for i in indexes] for _d, _i in zip(distances, indexes): lookup[_i+1] = _d @@ -97,13 +98,13 @@ def upload(name): query = { 'timing': time.time() - start, } - results = [ dataset.get_identity(index) for index in indexes ] + results = [ dataset.get_identity(id) for id in ids ] print(results) return jsonify({ 'results': results, - # 'distances': distances.tolist(), - # 'indexes': indexes.tolist(), + 'distances': distances.tolist(), + 'indexes': indexes.tolist(), }) @api.route('/dataset//name', methods=['GET']) diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index 54508f44..a01703d5 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -17,6 +17,23 @@ justify-content: flex-start; } +.results { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} +.results > div { + width: 200px; + margin-left: 20px; + margin-bottom: 40px; + font-size: 8pt; +} +.results > div img { + margin-bottom: 4px; +} +.results > div:nth-child(3n+1) { + margin-left: 0; +} .query h2 { margin-top: 0; padding-top: 0; } -- cgit v1.2.3-70-g09d2 From a2d4ad0499e4ee4f3f528b2ed7bc162d61026d09 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 01:13:32 +0100 Subject: build --- client/faceSearch/faceSearch.result.js | 14 +++++++++++++- megapixels/app/server/api.py | 14 ++++++++++---- site/assets/css/applets.css | 3 +++ 3 files changed, 26 insertions(+), 5 deletions(-) (limited to 'client/faceSearch') diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index 2b223a46..1882def0 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -5,10 +5,22 @@ import { courtesyS } from '../util' import * as actions from './faceSearch.actions' +const errors = { + 'bbox': "Sorry, we couldn't find a face in that image. Please choose an image where the face is large and clear.", + 'nomatch': "We didn't find a match.", + 'default': "There was an error!", +} + class FaceSearchResult extends Component { render() { const { dataset } = this.props.payload - const { distances, results } = this.props.result + const { distances, results, error } = this.props.result + if (error) { + let errorMessage = errors[error.message] || errors.default + return ( +
{errorMessage}
+ ) + } if (!results) { return (
diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 2f78ecd3..bc60118c 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -58,6 +58,10 @@ def upload(name): # get detection as BBox object bboxes = detector.detect(im_np, largest=True) + if not len(bboxes): + return jsonify({ + 'error': 'bbox' + }) bbox = bboxes[0] dim = im_np.shape[:2][::-1] bbox = bbox.to_dim(dim) # convert back to real dimensions @@ -73,16 +77,18 @@ def upload(name): distances, indexes = faiss_dataset.search(query, 10) if len(indexes) == 0: - print("weird, no results!") - return [] + return jsonify({ + 'error': 'nomatch' + }) # get the results for this single query... distances = distances[0] indexes = indexes[0] if len(indexes) == 0: - print("no results!") - return [] + return jsonify({ + 'error': 'nomatch' + }) lookup = {} ids = [i+1 for i in indexes] diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index a01703d5..ecba518c 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -30,6 +30,9 @@ } .results > div img { margin-bottom: 4px; + width: 200px; + height: 200px; + background: rgba(255,255,255,0.05); } .results > div:nth-child(3n+1) { margin-left: 0; -- cgit v1.2.3-70-g09d2 From 1c82f7ec6a603978322e16470547731e92e947c6 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 20:15:41 +0100 Subject: adding verbiage and timing --- client/faceSearch/faceSearch.result.js | 52 +++++++++++++++++++++++----- megapixels/app/models/sql_factory.py | 2 +- megapixels/app/server/api.py | 62 ++++++++++++++++++++-------------- megapixels/app/site/parser.py | 2 +- site/assets/css/applets.css | 5 +++ site/public/test/index.html | 16 ++++----- site/public/test/style/index.html | 10 ++++-- 7 files changed, 104 insertions(+), 45 deletions(-) (limited to 'client/faceSearch') diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index 1882def0..bc7831d9 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -4,31 +4,59 @@ import { connect } from 'react-redux' import { courtesyS } from '../util' import * as actions from './faceSearch.actions' +import { Loader } from '../common' const errors = { - 'bbox': "Sorry, we couldn't find a face in that image. Please choose an image where the face is large and clear.", - 'nomatch': "We didn't find a match.", - 'default': "There was an error!", + 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."} +
+ ), } class FaceSearchResult extends Component { render() { const { dataset } = this.props.payload - const { distances, results, error } = this.props.result + const { query, distances, results, loading, error } = this.props.result + if (loading) { + return ( +
+
+
+

Searching...

+
+
+ ) + } if (error) { - let errorMessage = errors[error.message] || errors.default + console.log(error) + let errorMessage = errors[error] || errors.error return (
{errorMessage}
) } if (!results) { return ( -
+
{errors.nomatch}
) } if (!this.props.result.results.length) { return ( -
No results
+
{errors.nomatch}
) } const els = results.map((result, i) => { @@ -48,7 +76,15 @@ class FaceSearchResult extends Component { 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}
diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 9a44941b..02b722df 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -98,7 +98,7 @@ class SqlDataset: return None session = Session() # for obj in session.query(table).filter_by(id=id): - print(table) + # print(table) obj = session.query(table).filter(table.id == id).first() session.close() return obj.toJSON() diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index bc60118c..b3447eb1 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -15,24 +15,32 @@ from app.utils.im_utils import pil2np sanitize_re = re.compile('[\W]+') valid_exts = ['.gif', '.jpg', '.jpeg', '.png'] +LIMIT = 9 +THRESHOLD = 0.3 + api = Blueprint('api', __name__) faiss_datasets = load_faiss_databases() @api.route('/') def index(): + """List the datasets and their fields""" return jsonify({ 'datasets': list_datasets() }) + @api.route('/dataset/') def show(name): + """Show the data that a dataset will return""" dataset = get_dataset(name) if dataset: return jsonify(dataset.describe()) else: return jsonify({ 'status': 404 }) + @api.route('/dataset//face/', methods=['POST']) def upload(name): + """Query an image against FAISS and return the matching identities""" start = time.time() dataset = get_dataset(name) if name not in faiss_datasets: @@ -52,31 +60,39 @@ def upload(name): im = Image.open(file.stream).convert('RGB') im_np = pil2np(im) - + # Face detection detector = face_detector.DetectorDLIBHOG() # get detection as BBox object bboxes = detector.detect(im_np, largest=True) - if not len(bboxes): + if not bboxes or not len(bboxes): return jsonify({ 'error': 'bbox' }) bbox = bboxes[0] + if not bbox: + return jsonify({ + 'error': 'bbox' + }) + dim = im_np.shape[:2][::-1] bbox = bbox.to_dim(dim) # convert back to real dimensions + print("got bbox") + if not bbox: + return jsonify({ + 'error': 'bbox' + }) - # face recognition/vector + # extract 128-D vector recognition = face_recognition.RecognitionDLIB(gpu=-1) vec = recognition.vec(im_np, bbox) - - # print(vec) query = np.array([ vec ]).astype('float32') - # query FAISS! - distances, indexes = faiss_dataset.search(query, 10) + # query FAISS + distances, indexes = faiss_dataset.search(query, LIMIT) - if len(indexes) == 0: + if len(indexes) == 0 or len(indexes[0]) == 0: return jsonify({ 'error': 'nomatch' }) @@ -85,36 +101,32 @@ def upload(name): distances = distances[0] indexes = indexes[0] - if len(indexes) == 0: - return jsonify({ - 'error': 'nomatch' - }) - - lookup = {} - ids = [i+1 for i in indexes] + dists = [] + ids = [] for _d, _i in zip(distances, indexes): - lookup[_i+1] = _d + if _d <= THRESHOLD: + dists.append(round(float(_d), 2)) + ids.append(_i+1) - print(distances) - print(indexes) + results = [ dataset.get_identity(_i) for _i in ids ] - # with the result we have an ID - # query the sql dataset for the UUID etc here + print(distances) + print(ids) query = { - 'timing': time.time() - start, + 'timing': round(time.time() - start, 3), } - results = [ dataset.get_identity(id) for id in ids ] - print(results) return jsonify({ + 'query': query, 'results': results, - 'distances': distances.tolist(), - 'indexes': indexes.tolist(), + 'distances': dists, }) + @api.route('/dataset//name', methods=['GET']) def name_lookup(dataset): + """Find a name in the dataset""" start = time.time() dataset = get_dataset(name) diff --git a/megapixels/app/site/parser.py b/megapixels/app/site/parser.py index ecfae0cb..b3d3a8c2 100644 --- a/megapixels/app/site/parser.py +++ b/megapixels/app/site/parser.py @@ -64,7 +64,7 @@ def format_applet(section, s3_path): else: command = payload[0] opt = None - if command == 'python': + if command == 'python' or command == 'javascript' or command == 'code': return format_section([ section ], s3_path) applet['command'] = command diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index ecba518c..edd5b709 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -18,6 +18,8 @@ } .results { + margin-top: 10px; + padding-bottom: 10px; display: flex; flex-direction: row; flex-wrap: wrap; @@ -27,6 +29,9 @@ margin-left: 20px; margin-bottom: 40px; font-size: 8pt; + background: #000; + padding: 5px; + font-weight: 500; } .results > div img { margin-bottom: 4px; diff --git a/site/public/test/index.html b/site/public/test/index.html index b4d16036..41f8eda5 100644 --- a/site/public/test/index.html +++ b/site/public/test/index.html @@ -30,14 +30,14 @@

Megapixels UI Tests

diff --git a/site/public/test/style/index.html b/site/public/test/style/index.html index 6d99a236..ab13a589 100644 --- a/site/public/test/style/index.html +++ b/site/public/test/style/index.html @@ -54,10 +54,16 @@
Person 3. Let me tell you about Person 3.  This person has a very long description with text which wraps like crazy
Person 3. Let me tell you about Person 3. This person has a very long description with text which wraps like crazy

est, qui dolorem ipsum, quia dolor sit amet consectetur adipisci[ng] velit, sed quia non-numquam [do] eius modi tempora inci[di]dunt, ut labore et dolore magnam aliquam quaerat voluptatem.

This image is extremely wide and the text beneath it will wrap but thats fine because it can also contain <a href="https://example.com/">hyperlinks</a>! Yes, you read that right—hyperlinks! Lorem ipsum dolor sit amet ad volotesque sic hoc ad nauseam
This image is extremely wide and the text beneath it will wrap but that's fine because it can also contain hyperlinks! Yes, you read that right—hyperlinks! Lorem ipsum dolor sit amet ad volotesque sic hoc ad nauseam

Inline code has back-ticks around it.

-
s = "Python syntax highlighting"
+
var s = "JavaScript syntax highlighting";
+alert(s);
+
+
s = "Python syntax highlighting"
 print(s)
 
-
tag."]}'>

Horizontal rule

+
Generic code block. Note that code blocks that are not so marked will not appear.
+But let's throw in a <b>tag</b>.
+
+

Horizontal rule


Citations below here

-- cgit v1.2.3-70-g09d2 From e67871d26f2e73861187e86110e240dd7718ea51 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 20:17:38 +0100 Subject: percentage --- client/faceSearch/faceSearch.result.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'client/faceSearch') diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index bc7831d9..2a85552d 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -64,12 +64,12 @@ class FaceSearchResult extends Component { const { uuid } = result.uuid const { fullname, gender, description, images } = result.identity return ( -
+
{fullname} {'('}{gender}{')'}
{description}
- {courtesyS(images, 'image')}
- {distance} + {courtesyS(images, 'image')}{' in dataset'}
+ {Math.round((1 - distance) * 100)}{'% match'}
) }) -- cgit v1.2.3-70-g09d2 From dfce989731fc58268b40280af79eeaa0b80b333e Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Mon, 17 Dec 2018 23:49:57 +0100 Subject: unicode --- client/faceSearch/faceSearch.result.js | 6 ++---- megapixels/app/server/api.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) (limited to 'client/faceSearch') diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index 2a85552d..d63f3265 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -50,11 +50,9 @@ class FaceSearchResult extends Component { ) } if (!results) { - return ( -
{errors.nomatch}
- ) + return
} - if (!this.props.result.results.length) { + if (!results.length) { return (
{errors.nomatch}
) diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 024c32cb..9c874d81 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -54,7 +54,7 @@ def upload(name): fn = 'filename.jpg' basename, ext = os.path.splitext(fn) - print("got {}, type {}".format(basename, ext)) + # print("got {}, type {}".format(basename, ext)) if ext.lower() not in valid_exts: return jsonify({ 'error': 'not an image' }) @@ -78,7 +78,7 @@ def upload(name): dim = im_np.shape[:2][::-1] bbox = bbox.to_dim(dim) # convert back to real dimensions - print("got bbox") + # print("got bbox") if not bbox: return jsonify({ 'error': 'bbox' @@ -110,13 +110,15 @@ def upload(name): results = [ dataset.get_identity(int(_i)) for _i in ids ] - print(distances) - print(ids) + # print(distances) + # print(ids) query = { + 'bbox': bboxes[0], + 'bbox_dim': bbox, 'timing': round(time.time() - start, 3), } - print(results) + # print(results) return jsonify({ 'query': query, 'results': results, @@ -138,7 +140,7 @@ def name_lookup(dataset): } results = [] - print(results) + # print(results) return jsonify({ 'query': query, 'results': results, -- cgit v1.2.3-70-g09d2 From 7b8e6f9a7d3eb36b72b53d5e754b9c7916b98ed7 Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 18 Dec 2018 01:03:10 +0100 Subject: namesearchg --- client/actions.js | 4 +- client/applet.js | 5 +- client/faceSearch/faceSearch.container.js | 2 +- client/faceSearch/faceSearch.result.js | 16 +++++- client/index.js | 1 - client/nameSearch/index.js | 5 ++ client/nameSearch/nameSearch.actions.js | 52 ++++++++++++++++++ client/nameSearch/nameSearch.container.js | 24 +++++++++ client/nameSearch/nameSearch.query.js | 48 +++++++++++++++++ client/nameSearch/nameSearch.reducer.js | 32 +++++++++++ client/nameSearch/nameSearch.result.js | 88 +++++++++++++++++++++++++++++++ client/store.js | 8 +-- client/tables.js | 4 ++ client/types.js | 3 ++ megapixels/app/models/sql_factory.py | 14 +++++ megapixels/app/server/api.py | 41 +++++++------- megapixels/app/settings/app_cfg.py | 2 +- site/assets/css/applets.css | 24 +++++++-- 18 files changed, 339 insertions(+), 34 deletions(-) create mode 100644 client/nameSearch/index.js create mode 100644 client/nameSearch/nameSearch.actions.js create mode 100644 client/nameSearch/nameSearch.container.js create mode 100644 client/nameSearch/nameSearch.query.js create mode 100644 client/nameSearch/nameSearch.reducer.js create mode 100644 client/nameSearch/nameSearch.result.js (limited to 'client/faceSearch') diff --git a/client/actions.js b/client/actions.js index bb011838..2be8229d 100644 --- a/client/actions.js +++ b/client/actions.js @@ -1,5 +1,7 @@ import * as faceSearch from './faceSearch/faceSearch.actions' +import * as nameSearch from './nameSearch/nameSearch.actions' export { - faceSearch + faceSearch, + nameSearch, } diff --git a/client/applet.js b/client/applet.js index 4d2a8e6c..80d40657 100644 --- a/client/applet.js +++ b/client/applet.js @@ -1,13 +1,16 @@ import React, { Component } from 'react' import { Container as FaceSearchContainer } from './faceSearch' +import { Container as NameSearchContainer } from './nameSearch' export default class Applet extends Component { render() { - console.log(this.props) + // console.log(this.props) switch (this.props.payload.cmd) { case 'face_search': return + case 'name_search': + return default: return
{'Megapixels'}
} diff --git a/client/faceSearch/faceSearch.container.js b/client/faceSearch/faceSearch.container.js index f96961db..94c6eb9f 100644 --- a/client/faceSearch/faceSearch.container.js +++ b/client/faceSearch/faceSearch.container.js @@ -10,7 +10,7 @@ import FaceSearchResult from './faceSearch.result' class FaceSearchContainer extends Component { render() { const { payload } = this.props - console.log(payload) + // console.log(payload) return (
diff --git a/client/faceSearch/faceSearch.result.js b/client/faceSearch/faceSearch.result.js index d63f3265..936bc8d2 100644 --- a/client/faceSearch/faceSearch.result.js +++ b/client/faceSearch/faceSearch.result.js @@ -32,6 +32,7 @@ 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 (
@@ -43,7 +44,7 @@ class FaceSearchResult extends Component { ) } if (error) { - console.log(error) + // console.log(error) let errorMessage = errors[error] || errors.error return (
{errorMessage}
@@ -60,10 +61,21 @@ 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 { 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'}
diff --git a/client/index.js b/client/index.js index 2beb5526..93341a77 100644 --- a/client/index.js +++ b/client/index.js @@ -38,7 +38,6 @@ function appendApplets(applets) { el.classList.add('loaded') break default: - console.log('react', el, payload) appendReactApplet(el, payload) el.classList.add('loaded') break diff --git a/client/nameSearch/index.js b/client/nameSearch/index.js new file mode 100644 index 00000000..8c6475e4 --- /dev/null +++ b/client/nameSearch/index.js @@ -0,0 +1,5 @@ +import Container from './nameSearch.container' + +export { + Container, +} diff --git a/client/nameSearch/nameSearch.actions.js b/client/nameSearch/nameSearch.actions.js new file mode 100644 index 00000000..290ee38d --- /dev/null +++ b/client/nameSearch/nameSearch.actions.js @@ -0,0 +1,52 @@ +// import fetchJsonp from 'fetch-jsonp' +import * as types from '../types' +// import { hashPath } from '../util' +import { post } from '../util' +// import querystring from 'query-string' + +// urls + +const url = { + search: (dataset, q) => process.env.API_HOST + '/api/dataset/' + dataset + '/name?q=' + encodeURIComponent(q), +} +export const publicUrl = { +} + +// standard loading events + +const loading = (tag, offset) => ({ + type: types.nameSearch.loading, + tag, + offset +}) +const loaded = (tag, data, offset = 0) => ({ + type: types.nameSearch.loaded, + tag, + data, + offset +}) +const error = (tag, err) => ({ + type: types.nameSearch.error, + tag, + err +}) + +// search UI functions + +export const updateOptions = opt => dispatch => { + dispatch({ type: types.nameSearch.update_options, opt }) +} + +// API functions + +export const search = (payload, q) => dispatch => { + const tag = 'result' + const fd = new FormData() + fd.append('q', q) + dispatch(loading(tag)) + post(url.search(payload.dataset, q), fd) + .then(data => { + dispatch(loaded(tag, data)) + }) + .catch(err => dispatch(error(tag, err))) +} diff --git a/client/nameSearch/nameSearch.container.js b/client/nameSearch/nameSearch.container.js new file mode 100644 index 00000000..b0de0c3a --- /dev/null +++ b/client/nameSearch/nameSearch.container.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from './nameSearch.actions' + +import NameSearchQuery from './nameSearch.query' +import NameSearchResult from './nameSearch.result' + +class NameSearchContainer extends Component { + render() { + const { payload } = this.props + // console.log(payload) + return ( +
+ + +
+ ) + } +} + + +export default NameSearchContainer diff --git a/client/nameSearch/nameSearch.query.js b/client/nameSearch/nameSearch.query.js new file mode 100644 index 00000000..b82e324b --- /dev/null +++ b/client/nameSearch/nameSearch.query.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import * as actions from './nameSearch.actions' + +class NameSearchQuery extends Component { + state = { + value: null + } + + handleInput(value) { + this.setState({ q: value }) + if (value.length > 2) { + this.props.actions.search(this.props.payload, value) + } + } + + render() { + return ( +
+

Find Your Name

+

Searching {13456} identities

+

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

+ this.handleInput(e.target.value)} + /> +
+ ) + } +} + +const mapStateToProps = state => ({ + result: state.nameSearch.result, + options: state.nameSearch.options, +}) + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(NameSearchQuery) diff --git a/client/nameSearch/nameSearch.reducer.js b/client/nameSearch/nameSearch.reducer.js new file mode 100644 index 00000000..101c93ea --- /dev/null +++ b/client/nameSearch/nameSearch.reducer.js @@ -0,0 +1,32 @@ +import * as types from '../types' + +const initialState = () => ({ + query: {}, + result: {}, + loading: false, +}) + +export default function nameSearchReducer(state = initialState(), action) { + switch (action.type) { + case types.nameSearch.loading: + return { + ...state, + [action.tag]: { loading: true }, + } + + case types.nameSearch.loaded: + return { + ...state, + [action.tag]: action.data, + } + + case types.nameSearch.error: + return { + ...state, + [action.tag]: { error: action.err }, + } + + default: + return state + } +} diff --git a/client/nameSearch/nameSearch.result.js b/client/nameSearch/nameSearch.result.js new file mode 100644 index 00000000..9e20228c --- /dev/null +++ b/client/nameSearch/nameSearch.result.js @@ -0,0 +1,88 @@ +import React, { Component } from 'react' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import { courtesyS } from '../util' + +import * as actions from './nameSearch.actions' +import { Loader } from '../common' + +const errors = { + nomatch: ( +
+

Name not found

+ {"No names matched your query."} +
+ ), + error: ( +
+

{"No matches found"}

+
+ ), +} + +class NameSearchResult extends Component { + render() { + const { dataset } = this.props.payload + const { query, results, loading, error } = this.props.result + console.log(this.props.result) + if (loading) { + return ( +
+
+ +
+
+ ) + } + 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 { uuid } = result.uuid + const { fullname, gender, description, images } = result.identity + return ( +
+ + {fullname} {'('}{gender}{')'}
+ {description}
+ {courtesyS(images, 'image')}{' in dataset'}
+
+ ) + }) + + return ( +
+
+ {'Search took '}{Math.round(query.timing * 1000) + ' ms'} +
+
+ {els} +
+
+ ) + } +} + +const mapStateToProps = state => ({ + query: state.nameSearch.query, + result: state.nameSearch.result, + options: state.nameSearch.options, +}) + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ ...actions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(NameSearchResult) diff --git a/client/store.js b/client/store.js index 03f983a5..13612f2d 100644 --- a/client/store.js +++ b/client/store.js @@ -1,16 +1,12 @@ import { applyMiddleware, compose, combineReducers, createStore } from 'redux' import thunk from 'redux-thunk' -// import metadataReducer from './metadata/metadata.reducer' import faceSearchReducer from './faceSearch/faceSearch.reducer' -// import reviewReducer from './review/review.reducer' +import nameSearchReducer from './nameSearch/nameSearch.reducer' const rootReducer = combineReducers({ - auth: (state = {}) => state, - // auth: (state = login()) => state, - // metadata: metadataReducer, faceSearch: faceSearchReducer, - // review: reviewReducer, + nameSearch: nameSearchReducer, }) function configureStore(initialState = {}) { diff --git a/client/tables.js b/client/tables.js index 2a2699f9..b4c13887 100644 --- a/client/tables.js +++ b/client/tables.js @@ -74,4 +74,8 @@ export default function append(el, payload) { } }) } + + if (fields.length > 1 && fields[1].indexOf('filter')) { + const filter = fields[1].split(' ') + } } diff --git a/client/types.js b/client/types.js index d295d0d1..fb1fbe30 100644 --- a/client/types.js +++ b/client/types.js @@ -10,5 +10,8 @@ export const faceSearch = tagAsType('faceSearch', [ 'loading', 'loaded', 'error', 'update_options', ]) +export const nameSearch = tagAsType('nameSearch', [ + 'loading', 'loaded', 'error', 'update_options', +]) export const init = '@@INIT' diff --git a/megapixels/app/models/sql_factory.py b/megapixels/app/models/sql_factory.py index 414ef3a6..da95b539 100644 --- a/megapixels/app/models/sql_factory.py +++ b/megapixels/app/models/sql_factory.py @@ -97,6 +97,20 @@ class SqlDataset: 'pose': self.select('pose', id), } + def search_name(self, q): + table = self.get_table('identity_meta') + uuid_table = self.get_table('uuids') + + identity = table.query.filter(table.fullname.like(q)).order_by(table.fullname.desc()).limit(30) + identities = [] + for row in identity: + uuid = uuid_table.query.filter(uuid_table.id == row.image_id).first() + identities.append({ + 'uuid': uuid.toJSON(), + 'identity': row.toJSON(), + }) + return identities + def select(self, table, id): table = self.get_table(table) if not table: diff --git a/megapixels/app/server/api.py b/megapixels/app/server/api.py index 8ff06611..33cf45df 100644 --- a/megapixels/app/server/api.py +++ b/megapixels/app/server/api.py @@ -28,26 +28,26 @@ def index(): return jsonify({ 'datasets': list_datasets() }) -@api.route('/dataset/') -def show(name): +@api.route('/dataset/') +def show(dataset_name): """Show the data that a dataset will return""" - dataset = get_dataset(name) + dataset = get_dataset(dataset_name) if dataset: return jsonify(dataset.describe()) else: return jsonify({ 'status': 404 }) -@api.route('/dataset//face/', methods=['POST']) -def upload(name): +@api.route('/dataset//face', methods=['POST']) +def upload(dataset_name): """Query an image against FAISS and return the matching identities""" start = time.time() - dataset = get_dataset(name) - if name not in faiss_datasets: + dataset = get_dataset(dataset_name) + if dataset_name not in faiss_datasets: return jsonify({ 'error': 'invalid dataset' }) - faiss_dataset = faiss_datasets[name] + faiss_dataset = faiss_datasets[dataset_name] file = request.files['query_img'] fn = file.filename if fn.endswith('blob'): @@ -106,17 +106,21 @@ def upload(name): for _d, _i in zip(distances, indexes): if _d <= THRESHOLD: dists.append(round(float(_d), 2)) - ids.append(_i) + ids.append(_i+1) results = [ dataset.get_identity(int(_i)) for _i in ids ] # print(distances) # print(ids) + # 'bbox': str(bboxes[0]), + # 'bbox_dim': str(bbox), + print(bboxes[0]) + print(bbox) + query = { - 'bbox': bboxes[0], - 'bbox_dim': bbox, 'timing': round(time.time() - start, 3), + 'bbox': str(bbox), } # print(results) return jsonify({ @@ -126,20 +130,21 @@ def upload(name): }) -@api.route('/dataset//name', methods=['GET']) -def name_lookup(dataset): +@api.route('/dataset//name', methods=['GET','POST']) +def name_lookup(dataset_name): """Find a name in the dataset""" start = time.time() - dataset = get_dataset(name) + dataset = get_dataset(dataset_name) - # we have a query from the request query string... - # use this to do a like* query on the identities_meta table + q = request.args.get('q') + print(q) query = { + 'q': q, 'timing': time.time() - start, } - results = [] - + results = dataset.search_name(q + '%') if q else None + # print(results) return jsonify({ 'query': query, diff --git a/megapixels/app/settings/app_cfg.py b/megapixels/app/settings/app_cfg.py index d7752739..55fed166 100644 --- a/megapixels/app/settings/app_cfg.py +++ b/megapixels/app/settings/app_cfg.py @@ -89,7 +89,7 @@ CKPT_ZERO_PADDING = 9 HASH_TREE_DEPTH = 3 HASH_BRANCH_SIZE = 3 -DLIB_FACEREC_JITTERS = 25 # number of face recognition jitters +DLIB_FACEREC_JITTERS = 5 # number of face recognition jitters DLIB_FACEREC_PADDING = 0.25 # default dlib POSE_MINMAX_YAW = (-25,25) diff --git a/site/assets/css/applets.css b/site/assets/css/applets.css index edd5b709..315d72e0 100644 --- a/site/assets/css/applets.css +++ b/site/assets/css/applets.css @@ -16,7 +16,15 @@ flex-direction: row; justify-content: flex-start; } - +.q { + width: 100%; + padding: 5px; + font-size: 14pt; +} +.timing { + font-size: 9pt; + padding-top: 10px; +} .results { margin-top: 10px; padding-bottom: 10px; @@ -34,9 +42,10 @@ font-weight: 500; } .results > div img { + display: block; margin-bottom: 4px; - width: 200px; - height: 200px; + width: 190px; + height: 190px; background: rgba(255,255,255,0.05); } .results > div:nth-child(3n+1) { @@ -45,6 +54,15 @@ .query h2 { margin-top: 0; padding-top: 0; } +.img { + position: relative; +} +.img .bbox { + position: absolute; + color: rgba(255,0,0,1); + background: rgba(255,0,0,0.05); + border: 1px solid; +} .cta { padding-left: 20px; font-size: 11pt; -- cgit v1.2.3-70-g09d2 From f9810daea17a90bfe40241719c835e5484d7403d Mon Sep 17 00:00:00 2001 From: Jules Laplace Date: Tue, 18 Dec 2018 01:13:42 +0100 Subject: bbox --- client/faceSearch/faceSearch.actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'client/faceSearch') diff --git a/client/faceSearch/faceSearch.actions.js b/client/faceSearch/faceSearch.actions.js index 224977b5..03e1a91d 100644 --- a/client/faceSearch/faceSearch.actions.js +++ b/client/faceSearch/faceSearch.actions.js @@ -8,7 +8,7 @@ import { post, preloadImage } from '../util' // urls const url = { - upload: (dataset) => process.env.API_HOST + '/api/dataset/' + dataset + '/face/', + upload: (dataset) => process.env.API_HOST + '/api/dataset/' + dataset + '/face', } export const publicUrl = { } -- cgit v1.2.3-70-g09d2