reverse image search

This commit is contained in:
qwertyforce 2020-09-14 23:11:10 +03:00
parent 69b40ef49d
commit 64265d39f7
9 changed files with 342 additions and 11 deletions

View File

@ -6,6 +6,9 @@ import Typography from '@material-ui/core/Typography';
import SearchIcon from '@material-ui/icons/Search';
import InputBase from '@material-ui/core/InputBase';
import Link from './Link'
import ImageSearchIcon from '@material-ui/icons/ImageSearch';
import { IconButton } from '@material-ui/core';
import { useRouter } from 'next/router'
const useStyles = makeStyles((theme) => ({
@ -94,6 +97,9 @@ export default function DenseAppBar() {
inputProps={{ 'aria-label': 'search' }}
/>
</div>
<IconButton color="inherit" aria-label="image_search" href="/reverse_search">
<ImageSearchIcon />
</IconButton>
</Toolbar>
</AppBar>
</div>

138
package-lock.json generated
View File

@ -2787,6 +2787,15 @@
"@types/node": "*"
}
},
"@types/multer": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-wdfkiKBBEMTODNbuF3J+qDDSqJxt50yB9pgDiTcFew7f97Gcc7/sM4HR66ofGgpJPOALWOqKAch4gPyqEXSkeQ==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/node": {
"version": "14.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.2.tgz",
@ -3416,6 +3425,11 @@
}
}
},
"append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@ -3588,6 +3602,11 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"attr-accept": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@ -4181,6 +4200,38 @@
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
"requires": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@ -5204,6 +5255,38 @@
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
"dev": true
},
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
"requires": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@ -6343,6 +6426,21 @@
"flat-cache": "^2.0.1"
}
},
"file-selector": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.13.tgz",
"integrity": "sha512-T2efCBY6Ps+jLIWdNQsmzt/UnAjKOEAlsZVdnQztg/BtAZGNL4uX1Jet9cMM8gify/x4CSudreji2HssGBNVIQ==",
"requires": {
"tslib": "^2.0.1"
},
"dependencies": {
"tslib": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
"integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
}
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@ -9395,6 +9493,16 @@
"object-visit": "^1.0.0"
}
},
"material-ui-dropzone": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/material-ui-dropzone/-/material-ui-dropzone-3.4.0.tgz",
"integrity": "sha512-c+H0+dQ65+252KJA3oIfcOXou78/+1nHAAakSZxmN9TudtZMoWnXXuUc8JRdM2zCy5p9kopUw5rG9o4/6ISPig==",
"requires": {
"@babel/runtime": "^7.4.4",
"clsx": "^1.0.2",
"react-dropzone": "^10.2.1"
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -9719,6 +9827,21 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multer": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz",
"integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==",
"requires": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.1",
"object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
}
},
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
@ -11512,6 +11635,16 @@
"scheduler": "^0.19.1"
}
},
"react-dropzone": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.2.tgz",
"integrity": "sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==",
"requires": {
"attr-accept": "^2.0.0",
"file-selector": "^0.1.12",
"prop-types": "^15.7.2"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -12755,6 +12888,11 @@
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
},
"streamsearch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
},
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",

View File

@ -27,7 +27,9 @@
"express-session": "^1.17.1",
"express-validator": "^6.6.1",
"imghash": "0.0.7",
"material-ui-dropzone": "^3.4.0",
"mongodb": "^3.6.1",
"multer": "^1.4.2",
"next": "latest",
"nodemailer": "^6.4.11",
"pm2": "^4.4.1",
@ -45,6 +47,7 @@
"@types/express-session": "^1.17.0",
"@types/grecaptcha": "^3.0.1",
"@types/mongodb": "^3.5.27",
"@types/multer": "^1.4.4",
"@types/node": "^14.6.2",
"@types/nodemailer": "^6.4.0",
"@types/react": "^16.9.48",

51
pages/reverse_search.tsx Normal file
View File

@ -0,0 +1,51 @@
import React, { useState } from 'react';
import Box from '@material-ui/core/Box';
import AppBar from '../components/AppBar'
import { DropzoneArea } from 'material-ui-dropzone';
import Button from '@material-ui/core/Button';
import config from '../config/config'
import axios from "axios"
import { useRouter } from 'next/router'
export default function ReverseSearch() {
const router = useRouter()
const [Files, setFiles] = useState([]);
const send_image = (token: string) => {
const formData = new FormData();
formData.append("image", Files[0]);
formData.append("g-recaptcha-response", token);
axios(`/reverse_search`, {
method: "post",
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
}).then((resp) => {
router.push("/show?ids="+resp.data.ids)
}).catch((err) => {
console.log(err)
})
}
const _send_image = () => {
/*global grecaptcha*/ // defined in public/index.html
grecaptcha.ready(function () {
grecaptcha.execute(config.recaptcha_site_key, { action: 'login' }).then(function (token) {
send_image(token)
});
})
}
return (
<div>
<AppBar />
<Box my={4}>
<DropzoneArea
acceptedFiles={['image/png', 'image/jpg', 'image/jpeg']}
dropzoneText={"Drag and drop an image here or click"}
filesLimit={1}
onChange={(files) => setFiles((files as never))}
/>
</Box>
<Button onClick={() => { _send_image() }} variant="contained" color="primary" >Reverse Search</Button>
</div>
);
}

61
pages/show.tsx Normal file
View File

@ -0,0 +1,61 @@
import React from "react";
import Gallery from "react-photo-gallery";
import { makeStyles } from '@material-ui/core/styles';
import config from '../config/config'
import AppBar from '../components/AppBar'
import db_ops from '../server/helpers/db_ops'
import { useRouter } from 'next/router'
import Photo from '../components/Photo'
import ErrorPage from 'next/error'
const useStyles = makeStyles(() => ({
pagination:{
display:"flex",
justifyContent:'center'
}
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function Show(props:any){
const classes = useStyles();
if (props.err) {
return <ErrorPage statusCode={404} />
}
return (
<div>
<AppBar/>
{/*
// @ts-ignore */ }
<Gallery targetRowHeight={250} photos={props.photos} renderImage={Photo} /> {/* FIX THIS SHIT */}
</div>
)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function getServerSideProps(context: any) {
if (context.query.ids) {
const ids = context.query.ids.split(',')
const images:Array<Record<string,unknown>>=[]
for(const id of ids){
const img_data=await db_ops.image_ops.find_image_by_id(parseInt(id))
images.push(img_data[0])
}
const photos=[]
for (const image of images){
photos.push({
src: `${config.domain}/images/${image.id}.${image.file_ext}`,
key: `${config.domain}/image/${image.id}`,
width: image.width,
height: image.height
})
}
return {
props: {
photos: photos
}
}
}
return {
props: { err: true }, // will be passed to the page component as props
}
}

View File

@ -91,8 +91,36 @@ async function generate_id() {
});
return id;
}
/////////////////////////////////////////////////IMAGE SEARCH OPS
async function get_all_phash_distances(){
const phash_distances = findDocuments("img_search", {})
return phash_distances
}
async function get_phash_distances_by_image_id(id:number){
const phash_distances = findDocuments("img_search", {id:id})
return phash_distances
}
async function add_image_to_image_search(id:number, phash_dist:Array<Record<string,unknown>>){
insertDocuments("img_search", [{
id:id,
phash_dist:phash_dist
}])
}
async function update_phash_dist_by_id(id:number, phash_dist:Array<Record<string,unknown>>){
updateDocument("images", {id: id},{phash_dist:phash_dist})
}
////////////////////////////////////////////////
/////////////////////////////////////////////////IMAGES OPS
async function get_ids_and_phashes(){
const collection = client.db(db_main).collection("images");
const data = collection.aggregate([{ $project : { id : 1, phash : 1,_id : 0} }]).toArray()
return data
}
async function update_image_data_by_id(id:number,update:Record<string,unknown>){
updateDocument("images", {id: id},update)
}
@ -274,15 +302,22 @@ async function create_new_user_not_activated(email:string, pass:string, token:st
/////////////////////////////////////////////////////////
export default {
image_ops:{
add_image,
get_all_images,
find_image_by_id,
get_max_image_id,
find_images_by_tags,
find_image_by_phash,
find_image_by_sha512,
update_image_data_by_id
image_ops: {
add_image,
get_all_images,
find_image_by_id,
get_max_image_id,
find_images_by_tags,
get_ids_and_phashes,
find_image_by_phash,
find_image_by_sha512,
update_image_data_by_id
},
image_search:{
get_all_phash_distances,
update_phash_dist_by_id,
add_image_to_image_search,
get_phash_distances_by_image_id,
},
password_recovery:{
update_user_password_by_id,

View File

@ -7,6 +7,7 @@ import connectMongo from 'connect-mongo';
const MongoStore = connectMongo(session);
import rateLimit from "express-rate-limit";
import cors from 'cors';
import multer from 'multer'
//import https from 'https';
//import path from 'path';
import { check } from 'express-validator';
@ -32,9 +33,12 @@ import forgot_password from './routes/forgot_password';
import activate_account_email from './routes/activate_account_email';
import update_image_data from './routes/update_image_data'
import import_from_derpi from './routes/import_from_derpi'
import reverse_search from './routes/reverse_search'
next_app.prepare().then(() => {
const app = express()
const api_router=express.Router()
const storage = multer.memoryStorage()
const upload = multer({ storage: storage,limits:{files:1,fileSize:50000000}}) //50MB
const limiter = rateLimit({
windowMs: 15 * 60, // 15 minutes
max: 200 // limit each IP to w00 requests per windowMs
@ -84,7 +88,7 @@ next_app.prepare().then(() => {
api_router.get('/auth/github/callback', github_oauth_callback)
api_router.get('/auth/google/callback', google_oauth_callback)
api_router.post('/reverse_search', [upload.single('image'),recaptcha.middleware.verify], reverse_search)
api_router.post('/update_image_data', update_image_data)
api_router.post('/import_from_derpi', import_from_derpi)

View File

@ -44,7 +44,7 @@ async function import_from_derpi(req: Request, res: Response) {
});
const parsed_author = await parse_author(derpi_data.tags)
const derpi_link = "https://derpibooru.org/images/" + derpi_data.id
const phash = await imghash.hash(`${PATH_TO_IMAGES}/${image_id}.${derpi_data.format.toLowerCase()}`, 16);
const phash = await imghash.hash(image, 16);
await db_ops.image_ops.add_image(image_id, derpi_data.format.toLowerCase(), derpi_data.width, derpi_data.height, parsed_author, derpi_data.size,
derpi_link, derpi_data.upvotes, derpi_data.downvotes, derpi_data.id, derpi_data.created_at,
derpi_data.source_url, derpi_data.tags, derpi_data.wilson_score, derpi_data.sha512_hash, phash, derpi_data.description)

View File

@ -0,0 +1,33 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-var-requires */
// import db_ops from './../helpers/db_ops'
import { Request, Response } from 'express';
import db_ops from '../helpers/db_ops'
const imghash: any = require('imghash');
function hamming_distance(str1: string, str2: string) {
let distance = 0;
for (let i = 0; i < str1.length; i += 1) {
if (str1[i] !== str2[i]) {
distance += 1;
}
}
return distance;
}
async function reverse_search(req: Request, res: Response) {
if (req.recaptcha?.error) {
return res.status(403).json({
message: "Captcha error"
})
}
const phash= await imghash.hash(req.file.buffer,16)
const images=await db_ops.image_ops.get_ids_and_phashes()
for(let i=0;i<images.length;i++){
images[i].dist=hamming_distance(phash,images[i].phash)
}
images.sort((a,b)=>a.dist-b.dist)
images.length=30
const ids=images.map((el)=>el.id)
res.json({ids:ids.join(',')})
}
export default reverse_search;