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.query.js | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 client/faceSearch/faceSearch.query.js (limited to 'client/faceSearch/faceSearch.query.js') 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) -- 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/faceSearch.query.js') 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/faceSearch.query.js') 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/faceSearch.query.js') 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